Django - RBAC权限管理
第一版
表的设计
from django.db import models class Permission(models.Model): title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限',blank=True) def __str__(self): return self.name class User(models.Model): name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色',blank=True) def __str__(self): return self.name
使用admin录入权限,角色,用户
from django.contrib import admin from rbac import models # Register your models here. class PermissionAdmin(admin.ModelAdmin): list_display = ['title', 'url'] # 可查看 list_editable = ['url'] # 可做编辑 admin.site.register(models.Permission, PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.User)
登录相关视图
from django.shortcuts import render, HttpResponse, redirect, reverse from rbac import models from django.conf import settings def login(request): if request.method == 'POST': username = request.POST.get('username') pwd = request.POST.get('pwd') user = models.User.objects.filter(name=username, password=pwd).first() if not user: err_msg = '用户名或密码错误' return render(request, 'login.html', {'err_msg': err_msg}) # 登录成功 # 将权限信息写入到session # 1. 查当前登录用户拥有的权限 # values_list内部转为元组 permission_list = user.roles.filter(permissions__url__isnull=False).values_list( 'permissions__url').distinct() for i in permission_list: print(i) # 2. 将权限信息写入到session # 方法一 # request.session['permissions'] = list(permission_list) # [(),] 写入到session,session内部会序列化[[],] print("permission_list", permission_list) print("permission_list_1", list(permission_list)) # 方法二 写成可配置的 request.session[settings.PERMISSION_SESSION_KEY] = list(permission_list) return redirect(reverse('customer')) return render(request, 'login.html')
settings配置权限相关信息和白名单
# 中间件注册 MIDDLEWARE = [ 'web.middlewares.rbac.PermissionMiddleware', ] # ###### 权限相关的配置 ###### PERMISSION_SESSION_KEY = 'permissions' ### 这里需要用到正则 WHITE_URL_LIST = [ r'^/login/$', r'^/logout/$', r'^/reg/$', r'^/admin/.*', ]
中间件对权限的校验以及白名单的判断
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import HttpResponse import re class PermissionMiddleware(MiddlewareMixin): def process_request(self, request): # 对权限进行校验 # 1. 当前访问的URL current_url = request.path_info # 白名单的判断 for i in settings.WHITE_URL_LIST: if re.match(i,current_url): return # 2. 获取当前用户的所有权限信息 permission_list = request.session.get(settings.PERMISSION_SESSION_KEY) # 3. 权限的校验 print(current_url) for item in permission_list: url = item[0] if re.match("^{}$".format(url), current_url): return else: return HttpResponse('没有权限')
业务相关views和templates
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') money = models.IntegerField(verbose_name='付费金额') create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
from django.conf.urls import url from web.views import customer from web.views import payment from web.views import account urlpatterns = [ url(r'^customer/list/$', customer.customer_list,name='customer'), url(r'^customer/add/$', customer.customer_add), url(r'^customer/edit/(?P<cid>\d+)/$', customer.customer_edit), url(r'^customer/del/(?P<cid>\d+)/$', customer.customer_del), url(r'^payment/list/$', payment.payment_list), url(r'^payment/add/$', payment.payment_add), url(r'^payment/edit/(?P<pid>\d+)/$', payment.payment_edit), url(r'^payment/del/(?P<pid>\d+)/$', payment.payment_del), url(r'^login/$',account.login) ]
客户相关view
import os import mimetypes from django.shortcuts import render, redirect from django.http import FileResponse from django.conf import settings # import xlrd from web import models from web.forms.customer import CustomerForm def customer_list(request): """ 客户列表 :return: """ data_list = models.Customer.objects.all() return render(request, 'customer_list.html', {'data_list': data_list}) def customer_add(request): """ 编辑客户 :return: """ if request.method == 'GET': form = CustomerForm() return render(request, 'customer_edit.html', {'form': form}) form = CustomerForm(data=request.POST) if form.is_valid(): form.save() return redirect('/customer/list/') return render(request, 'customer_edit.html', {'form': form}) def customer_edit(request, cid): """ 新增客户 :return: """ obj = models.Customer.objects.get(id=cid) if request.method == 'GET': form = CustomerForm(instance=obj) return render(request, 'customer_add.html', {'form': form}) form = CustomerForm(data=request.POST, instance=obj) if form.is_valid(): form.save() return redirect('/customer/list/') return render(request, 'customer_add.html', {'form': form}) def customer_del(request, cid): """ 删除客户 :param request: :param cid: :return: """ models.Customer.objects.filter(id=cid).delete() return redirect('/customer/list/')
缴费相关view
#!/usr/bin/env python # -*- coding:utf-8 -*- from django.shortcuts import render, redirect from web import models from web.forms.payment import PaymentForm, PaymentUserForm def payment_list(request): """ 付费列表 :return: """ data_list = models.Payment.objects.all() return render(request, 'payment_list.html', {'data_list': data_list}) def payment_add(request): """ 编辑付费记录 :return: """ if request.method == 'GET': form = PaymentForm() return render(request, 'payment_edit.html', {'form': form}) form = PaymentForm(data=request.POST) if form.is_valid(): form.save() return redirect('/payment/list/') return render(request, 'payment_edit.html', {'form': form}) def payment_edit(request, pid): """ 新增付费记录 :return: """ obj = models.Payment.objects.get(id=pid) if request.method == 'GET': form = PaymentForm(instance=obj) return render(request, 'payment_add.html', {'form': form}) form = PaymentForm(data=request.POST, instance=obj) if form.is_valid(): form.save() return redirect('/payment/list/') return render(request, 'payment_add.html', {'form': form}) def payment_del(request, pid): """ 删除付费记录 :param request: :param cid: :return: """ models.Payment.objects.filter(id=pid).delete() return redirect('/payment/list/')
templates
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>爱软测</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} "> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } .left-menu .menu-body .static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .left-menu .menu-body .static-menu a { text-decoration: none; padding: 8px 15px; border-bottom: 1px solid #ccc; color: #333; display: block; background: #efefef; background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa)); background: -ms-linear-gradient(bottom, #efefef, #fafafa); background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%); background: -o-linear-gradient(bottom, #efefef, #fafafa); filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')"; box-shadow: inset 0px 1px 1px white; } .left-menu .menu-body .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .left-menu .menu-body .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } </style> </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">爱软测</span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">软测管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> <div class="static-menu"> <a href="/customer/list/" class="active"> <span class="icon-wrap"><i class="fa fa-connectdevelop"></i></span> 客户管理</a> <a href="/payment/list/"> <span class="icon-wrap"><i class="fa fa-code-fork"></i></span> 账单管理</a> </div> </div> </div> <div class="right-body"> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> <li><a href="#">首页</a></li> <li class="active">客户管理</li> </ol> </div> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> {% block js %} {% endblock %} </body> </html>
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta http-equiv="content-Type" charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <form action="" method="post"> {% csrf_token %} <p> 用户名:<input type="text" name="username"> </p> <p> 密码:<input type="password" name="pwd"> </p> <button>登录</button> <span style="color: red">{{ err_msg }}</span> </form> </body> </html>
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> <a class="btn btn-default" href="/customer/add/"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> </div> <table class="table table-bordered table-hover"> <thead> <tr> <th>ID</th> <th>客户姓名</th> <th>年龄</th> <th>邮箱</th> <th>公司</th> <th>选项</th> </tr> </thead> <tbody> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.name }}</td> <td>{{ row.age }}</td> <td>{{ row.email }}</td> <td>{{ row.company }}</td> <td> <a style="color: #333333;" href="/customer/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i></a> | <a style="color: #d9534f;" href="/customer/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a> </td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form method="post" enctype="multipart/form-data"> {% csrf_token %} <div class="form-group"> <div style="position: relative;display: inline-block;height: 50px;min-width: 300px;overflow: hidden;"> <div style="position: absolute;top: 0;left: 0;right: 0;bottom: 0;z-index: 1000;border: 1px dotted #9d9d9d;color: #9d9d9d;line-height: 50px;padding-left: 15px;"> <i class="fa fa-cloud-upload" aria-hidden="true"></i> <span>点击上传Excel文件</span> </div> <input name="customer_excel" type="file" id="excelFile" style="position: absolute;top: 0;left: 0;right: 0;bottom: 0;background-color: #333333;z-index: 1001;opacity: 0;filter:alpha(opacity=0);"> </div> <p class="help-block">注意:批量导入的Excel需使用规定格式模板. <a href="/customer/tpl/">下载模板</a></p> </div> <button type="submit" class="btn btn-primary">上传</button> {% if status %} <span style="color: green;">{{ msg }}</span> {% else %} <span style="color: red;">{{ msg }}</span> {% endif %} </form> </div> {% endblock %} {% block js %} <script> $(function () { $('#excelFile').change(function (e) { var fileName = e.currentTarget.files[0].name; $(this).prev().find('span').text(fileName); }) }) </script> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <div style="margin: 5px 0;"> <a class="btn btn-success" href="/payment/add/"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加缴费记录 </a> </div> <table class="table table-bordered table-hover"> <thead> <tr> <th>ID</th> <th>客户姓名</th> <th>金额</th> <th>付费时间</th> <th>选项</th> </tr> </thead> <tbody> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.customer.name }}</td> <td>{{ row.money }}</td> <td>{{ row.create_time|date:"Y-m-d H:i:s" }}</td> <td> <a style="color: #333333;" href="/payment/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i></a> | <a style="color: #d9534f;" href="/payment/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a> </td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
第二版:动态生成一级菜单
表结构的设计改动,增加菜单和图标标识
from django.db import models class Permission(models.Model): """ 权限表 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') is_menu = models.BooleanField(default=False, verbose_name='是否是菜单') icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name class User(models.Model): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name
admin的增加
from django.contrib import admin from rbac import models class PermissionAdmin(admin.ModelAdmin): list_display = ['title', 'url', 'is_menu', 'icon'] list_editable = ['url', 'is_menu', 'icon'] admin.site.register(models.Permission, PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.User)
登录
from django.shortcuts import render, HttpResponse, redirect, reverse from rbac import models from rbac.server.init_permission import init_permission import copy def login(request): if request.method == 'POST': username = request.POST.get('username') pwd = request.POST.get('pwd') user = models.User.objects.filter(name=username, password=pwd).first() if not user: err_msg = '用户名或密码错误' return render(request, 'login.html', {'err_msg': err_msg}) # 登录成功 # 将权限信息写入到session init_permission(request,user) return redirect(reverse('customer')) return render(request, 'login.html')
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限,values 返回{} permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__is_menu', 'permissions__icon', 'permissions__title').distinct() # 存放权限信息 permission_list = [] # 存放菜单信息 menu_list = [] for item in permission_query: permission_list.append({'url': item['permissions__url']}) if item.get('permissions__is_menu'): menu_list.append({'url': item['permissions__url'], 'icon': item['permissions__icon'], 'title': item['permissions__title']}) # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_list # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_list
中间件的校验
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import HttpResponse import re class PermissionMiddleware(MiddlewareMixin): def process_request(self, request): # 对权限进行校验 # 1. 当前访问的URL current_url = request.path_info # 白名单的判断 for i in settings.WHITE_URL_LIST: if re.match(i,current_url): return # 2. 获取当前用户的所有权限信息 permission_list = request.session.get(settings.PERMISSION_SESSION_KEY) # 3. 权限的校验 print(current_url) for item in permission_list: url = item['url'] if re.match("^{}$".format(url), current_url): return else: return HttpResponse('没有权限')
菜单展示逻辑判断,写到自定义inclusion_tag里面,在传到menu.html文件中
from django import template register = template.Library() from django.conf import settings import re @register.inclusion_tag('rbac/menu.html') def menu(request): menu_list = request.session.get(settings.MENU_SESSION_KEY) for item in menu_list: url = item['url'] if re.match('^{}$'.format(url), request.path_info): item['class'] = 'active' break return {"menu_list": menu_list}
<div class="static-menu"> {% for item in menu_list %} <a href="{{ item.url }}" class="{{ item.class }}"> <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</a> {% endfor %}
母版中引用menu.html
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>爱软测</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} "> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'css/menu.css' %}"> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } </style> </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">爱软测 </span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">软测管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% load rbac %} {% menu request %} </div> </div> <div class="right-body"> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> <li><a href="#">首页</a></li> <li class="active">客户管理</li> </ol> </div> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> {% block js %} {% endblock %} </body> </html>
应用到项目中
应用rbac组件 1. 拷贝rbac组件 到 新的项目中 并注册APP 2. 配置权限的相关信息 # ###### 权限相关的配置 ###### PERMISSION_SESSION_KEY = 'permissions' MENU_SESSION_KEY = 'menus' WHITE_URL_LIST = [ r'^/login/$', r'^/logout/$', r'^/reg/$', r'^/admin/.*', ] 3. 创建跟权限相关的表(删除之前的迁移文件的记录) 执行命令: python manage.py makemigrations python manage.py migrate 4. 录入权限信息 创建超级用户 录入所有权限信息 创建角色 给角色分权限 创建用户 给用户分角色 5. 在登录成功之后 写入权限和菜单的信息 到session中 6. 配置上中间件 进行权限的校验 7. 使用动态的菜单 导入静态文件 <link rel="stylesheet" href="{% static 'css/menu.css' %}"> 使用inclusion_tag <div class="left-menu"> <div class="menu-body"> {% load rbac %} {% menu request %} </div> </div>
第三版:动态生成二级菜单
表的设计
from django.db import models class Menu(models.Model): """ 一级菜单 """ title = models.CharField(max_length=32, unique=True) icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) class Meta: verbose_name_plural = '菜单表' verbose_name = '菜单表' def __str__(self): return self.title class Permission(models.Model): """ 权限表 有关联Menu的是二级菜单 没有关联Menu的不是二级菜单,是不可以做菜单的权限 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') menu = models.ForeignKey('Menu', null=True, blank=True) class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name class User(models.Model): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name
使用admin录入一级和二级菜单
from django.contrib import admin from rbac import models class PermissionAdmin(admin.ModelAdmin): list_display = ['title', 'url', ] list_editable = ['url', ] admin.site.register(models.Permission, PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.User) admin.site.register(models.Menu)
菜单的数据结构设计
data = [ { 'permissions__url': '/customer/list/', 'permissions__title': '客户列表', 'permissions__menu_id': 1, 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa-clipboard' }, { 'permissions__url': '/customer/add/', 'permissions__title': '添加客户', 'permissions__menu_id': None, 'permissions__menu__title': None, 'permissions__menu__icon': None }, { 'permissions__url': '/customer/edit/(?P<cid>\\d+)/', 'permissions__title': '编辑客户', 'permissions__menu_id': None, 'permissions__menu__title': None, 'permissions__menu__icon': None }, { 'permissions__url': '/payment/list/', 'permissions__title': '缴费列表', 'permissions__menu_id': 1, 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa-clipboard' }, ] menu_dict = {} for item in data: menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url']} ] } else: menu_dict[menu_id]['children'].append({'title': item['permissions__title'], 'url': item['permissions__url']}) print(menu_dict) """ { 1 : { 'title' : '信息管理', 'icon' : 'fa-clipboard', 'children': [ {'title':'客户列表','url':'/customer/list/'} ] }, 2 : { 'title' : '财务管理', 'icon' : 'fa-clipboard', 'children': [ {'title':'客户列表','url':'/customer/list/'} ] } } """
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限 permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__title', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', ).distinct() # 存放权限信息 permission_list = [] # 存放菜单信息 menu_dict = {} for item in permission_query: permission_list.append({'url': item['permissions__url']}) menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url']} ] } else: menu_dict[menu_id]['children'].append( {'title': item['permissions__title'], 'url': item['permissions__url']}) # # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_list # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_dict
菜单展示逻辑判断,写到自定义inclusion_tag里面,在传到menu.html文件中
from django import template register = template.Library() from django.conf import settings import re @register.inclusion_tag('rbac/menu.html') def menu(request): menu_list = request.session.get(settings.MENU_SESSION_KEY) return {"menu_list": menu_list}
{#<div class="static-menu">#} {##} {# {% for item in 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>#} <div class="multi-menu"> {% for item in menu_list.values %} <div class="item"> <div class="title"><i class="fa {{ item.icon }}"></i> {{ item.title }}</div> <div class="body hide"> {% for child in item.children %} <a href="{{ child.url }}">{{ child.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
菜单根据权重排序,二级菜单默认选中及展开
表字段的增加
from django.db import models class Menu(models.Model): """ 一级菜单 """ title = models.CharField(max_length=32, unique=True) icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) weight = models.IntegerField(default=1) class Meta: verbose_name_plural = '菜单表' verbose_name = '菜单表' def __str__(self): return self.title class Permission(models.Model): """ 权限表 有关联Menu的是二级菜单 没有关联Menu的不是二级菜单,是不可以做菜单的权限 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') menu = models.ForeignKey('Menu', null=True, blank=True) parent = models.ForeignKey('Permission', null=True, blank=True) class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name class User(models.Model): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name
menu.js
$('.item .title').click(function () { // $(this).next().toggleClass('hide') $(this).next().removeClass('hide'); $(this).parent().siblings().find('.body').addClass('hide'); });
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限 permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__title', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', 'permissions__menu__weight', ).distinct() # 存放权限信息 permission_list = [] # 存放菜单信息 menu_dict = {} for item in permission_query: permission_list.append({'url': item['permissions__url']}) menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'weight': item['permissions__menu__weight'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url']} ] } else: menu_dict[menu_id]['children'].append( {'title': item['permissions__title'], 'url': item['permissions__url']}) # # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_list # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_dict
自定义inclusion_tag
from django import template register = template.Library() from django.conf import settings import re from collections import OrderedDict @register.inclusion_tag('rbac/menu.html') def menu(request): menu_list = request.session.get(settings.MENU_SESSION_KEY) order_dict = OrderedDict() #python3.7以下是无序字典,使用OrderedDict # for i in sorted(menu_list, key=lambda x: menu_list[x]['weight'],reverse=True): # order_dict[i] = menu_list[i] # # for item in order_dict.values(): # item['class'] = 'hide' # # for i in item['children']: # # if re.match("^{}$".format(i['url']), request.path_info): # i['class'] = 'active' # item['class'] = '' for key in sorted(menu_list, key=lambda x: menu_list[x]['weight'], reverse=True): order_dict[key] = menu_list[key] item = order_dict[key] item['class'] = 'hide' for i in item['children']: if re.match("^{}$".format(i['url']), request.path_info): i['class'] = 'active' item['class'] = '' return {"menu_list": order_dict}
{#<div class="static-menu">#} {##} {# {% for item in 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>#} <div class="multi-menu"> {% for item in menu_list.values %} <div class="item"> <div class="title"><i class="fa {{ item.icon }}"></i> {{ item.title }}</div> <div class="body {{ item.class }}"> {% for child in item.children %} <a href="{{ child.url }}" class="{{ child.class }}">{{ child.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
第四版:三级菜单
访问子权限时父权限展开
表的设计
from django.db import models class Menu(models.Model): """ 一级菜单 """ title = models.CharField(max_length=32, unique=True) icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) weight = models.IntegerField(default=1) class Meta: verbose_name_plural = '菜单表' verbose_name = '菜单表' def __str__(self): return self.title class Permission(models.Model): """ 权限表 有关联Menu的是二级菜单 没有关联Menu的不是二级菜单,是不可以做菜单的权限 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') menu = models.ForeignKey('Menu', null=True, blank=True) parent = models.ForeignKey('Permission', null=True, blank=True) class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name class User(models.Model): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限 permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__title', 'permissions__id', 'permissions__parent_id', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', 'permissions__menu__weight', ).distinct() # 存放权限信息 permission_list = [] # 存放菜单信息 menu_dict = {} for item in permission_query: permission_list.append( {'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']}) menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'weight': item['permissions__menu__weight'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']} ] } else: menu_dict[menu_id]['children'].append( {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']}) # # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_list # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_dict
中间件校验
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import HttpResponse import re class PermissionMiddleware(MiddlewareMixin): def process_request(self, request): # 对权限进行校验 # 1. 当前访问的URL current_url = request.path_info # 白名单的判断 for i in settings.WHITE_URL_LIST: if re.match(i, current_url): return # 2. 获取当前用户的所有权限信息 permission_list = request.session.get(settings.PERMISSION_SESSION_KEY) # 3. 权限的校验 print(current_url) for item in permission_list: url = item['url'] if re.match("^{}$".format(url), current_url): pid = item['pid'] id = item['id'] if pid: # 表示当前权限是子权限,让父权限是展开 request.current_menu_id = pid print() else: # 表示当前权限是父权限,要展开的二级菜单 request.current_menu_id = id return else: return HttpResponse('没有权限')
自定义inclusion_tag
from django import template register = template.Library() from django.conf import settings import re from collections import OrderedDict '''实现了对字典对象中元素的排序''' @register.inclusion_tag('rbac/menu.html') def menu(request): menu_list = request.session.get(settings.MENU_SESSION_KEY) order_dict = OrderedDict() #有序字典 for key in sorted(menu_list, key=lambda x: menu_list[x]['weight'], reverse=True): order_dict[key] = menu_list[key] item = order_dict[key] item['class'] = 'hide' for i in item['children']: if i['id'] == request.current_menu_id: #当前二级菜单的id等于中间件初始化传过来的id或pid i['class'] = 'active' item['class'] = '' return {"menu_list": order_dict}
路径导航
中间件校验
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import HttpResponse import re class PermissionMiddleware(MiddlewareMixin): def process_request(self, request): # 对权限进行校验 # 1. 当前访问的URL current_url = request.path_info # 白名单的判断 for i in settings.WHITE_URL_LIST: if re.match(i, current_url): return # 2. 获取当前用户的所有权限信息 permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY) request.breadcrumb_list = [ {"title": '首页', 'url': '#'}, ] # 3. 权限的校验 print(permission_dict) for item in permission_dict.values(): url = item['url'] if re.match("^{}$".format(url), current_url): pid = item['pid'] id = item['id'] if pid: # 表示当前权限是子权限,让父权限是展开 request.current_menu_id = pid request.breadcrumb_list.extend( [{"title": permission_dict[str(pid)]['title'], 'url': permission_dict[str(pid)]['url']}, {"title": item['title'], 'url': item['url']}] ) else: # 表示当前权限是父权限,要展开的二级菜单 request.current_menu_id = id # 添加面包屑导航 request.breadcrumb_list.append({"title": item['title'], 'url': item['url']}) return else: return HttpResponse('没有权限')
权限数据结构更改
import json data = { 1: {'url': '/customer/list/', 'id': 1, 'pid': None, 'title': '客户列表'}, 2: {'url': '/customer/add/', 'id': 2, 'pid': 1, 'title': '添加客户'}, 3: {'url': '/customer/edit/(?P<cid>\\d+)/', 'id': 3, 'pid': 1, 'title': '编辑客户'}, 4: {'url': '/customer/del/(?P<cid>\\d+)/', 'id': 4, 'pid': 1, 'title': '删除客户'}, 5: {'url': '/payment/list/', 'id': 5, 'pid': None, 'title': '缴费列表'}, 6: {'url': '/payment/add/', 'id': 6, 'pid': 5, 'title': '添加缴费记录'}, 7: {'url': '/payment/edit/(?P<pid>\\d+)/', 'id': 7, 'pid': 5, 'title': '编辑缴费记录'}, 8: {'url': '/payment/del/(?P<pid>\\d+)/', 'id': 8, 'pid': 5, 'title': '删除缴费记录'}} # data2 = {'1': {'url': '/customer/list/', 'id': 1, 'pid': None, 'title': '客户列表'}, # '2': {'url': '/customer/add/', 'id': 2, 'pid': 1, 'title': '添加客户'}, # '3': {'url': '/customer/edit/(?P<cid>\\d+)/', 'id': 3, 'pid': 1, 'title': '编辑客户'}, # '4': {'url': '/customer/del/(?P<cid>\\d+)/', 'id': 4, 'pid': 1, 'title': '删除客户'}, # '5': {'url': '/payment/list/', 'id': 5, 'pid': None, 'title': '缴费列表'}, # '6': {'url': '/payment/add/', 'id': 6, 'pid': 5, 'title': '添加缴费记录'}, # '7': {'url': '/payment/edit/(?P<pid>\\d+)/', 'id': 7, 'pid': 5, 'title': '编辑缴费记录'}, # '8': {'url': '/payment/del/(?P<pid>\\d+)/', 'id': 8, 'pid': 5, 'title': '删除缴费记录'}} ret = json.dumps(data) print(json.loads(ret))
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限 permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__title', 'permissions__id', 'permissions__parent_id', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', 'permissions__menu__weight', ).distinct() # 存放权限信息 permission_dict = {} # 存放菜单信息 menu_dict = {} for item in permission_query: permission_dict[item['permissions__id']] = {'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id'], 'title': item['permissions__title']} menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'weight': item['permissions__menu__weight'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']} ] } else: menu_dict[menu_id]['children'].append( {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']}) # # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_dict # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_dict
自定义inclusion_tag
from django import template register = template.Library() from django.conf import settings import re from collections import OrderedDict @register.inclusion_tag('rbac/menu.html') def menu(request): menu_dict = request.session.get(settings.MENU_SESSION_KEY) order_dict = OrderedDict() for key in sorted(menu_dict, key=lambda x: menu_dict[x]['weight'], reverse=True): order_dict[key] = menu_dict[key] item = order_dict[key] item['class'] = 'hide' for i in item['children']: if i['id'] == request.current_menu_id: i['class'] = 'active' item['class'] = '' return {"menu_list": order_dict} @register.inclusion_tag('rbac/breadcrumb.html') def breadcrumb(request): return {'breadcrumb_list':request.breadcrumb_list}
<ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> {% for li in breadcrumb_list %} {% if forloop.last %} <li>{{ li.title }}</li> {% else %} <li><a href="{{ li.url }}">{{ li.title }}</a></li> {% endif %} {% endfor %} </ol>
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>爱软测</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} "> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'css/menu.css' %}"> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } </style> </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">爱软测 </span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">软测管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% load rbac %} {% menu request %} </div> </div> <div class="right-body"> <div> {% breadcrumb request %} </div> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="{% static 'js/menu.js' %} "></script> {% block js %} {% endblock %} </body> </html>
权限控制到按钮级别
表的设计
from django.db import models class Menu(models.Model): """ 一级菜单 """ title = models.CharField(max_length=32, unique=True) icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) weight = models.IntegerField(default=1) class Meta: verbose_name_plural = '菜单表' verbose_name = '菜单表' def __str__(self): return self.title class Permission(models.Model): """ 权限表 有关联Menu的是二级菜单 没有关联Menu的不是二级菜单,是不可以做菜单的权限 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') menu = models.ForeignKey('Menu', null=True, blank=True) parent = models.ForeignKey('Permission', null=True, blank=True) name = models.CharField(max_length=32, null=True, blank=True, unique=True) class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name class User(models.Model): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限 permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__title', 'permissions__id', 'permissions__name', 'permissions__parent_id', 'permissions__parent__name', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', 'permissions__menu__weight', ).distinct() # 存放权限信息 permission_dict = {} # 存放菜单信息 menu_dict = {} for item in permission_query: permission_dict[item['permissions__name']] = {'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id'], 'pname': item['permissions__parent__name'], 'title': item['permissions__title']} menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'weight': item['permissions__menu__weight'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']} ] } else: menu_dict[menu_id]['children'].append( {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']}) # # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_dict # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_dict
中间件的校验
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import HttpResponse import re class PermissionMiddleware(MiddlewareMixin): def process_request(self, request): # 对权限进行校验 # 1. 当前访问的URL current_url = request.path_info # 白名单的判断 for i in settings.WHITE_URL_LIST: if re.match(i, current_url): return # 2. 获取当前用户的所有权限信息 permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY) request.breadcrumb_list = [ {"title": '首页', 'url': '#'}, ] # 3. 权限的校验 print(permission_dict) for item in permission_dict.values(): url = item['url'] if re.match("^{}$".format(url), current_url): pid = item['pid'] id = item['id'] pname = item['pname'] if pid: # 表示当前权限是子权限,让父权限是展开 request.current_menu_id = pid request.breadcrumb_list.extend( [{"title": permission_dict[pname]['title'], 'url': permission_dict[pname]['url']}, {"title": item['title'], 'url': item['url']}] ) else: # 表示当前权限是父权限,要展开的二级菜单 request.current_menu_id = id # 添加面包屑导航 request.breadcrumb_list.append({"title": item['title'], 'url': item['url']}) return else: return HttpResponse('没有权限')
自定义inclusion_tag
from django import template register = template.Library() from django.conf import settings import re from collections import OrderedDict @register.inclusion_tag('rbac/menu.html') def menu(request): menu_dict = request.session.get(settings.MENU_SESSION_KEY) order_dict = OrderedDict() for key in sorted(menu_dict, key=lambda x: menu_dict[x]['weight'], reverse=True): order_dict[key] = menu_dict[key] item = order_dict[key] item['class'] = 'hide' for i in item['children']: if i['id'] == request.current_menu_id: i['class'] = 'active' item['class'] = '' return {"menu_list": order_dict} @register.inclusion_tag('rbac/breadcrumb.html') def breadcrumb(request): return {'breadcrumb_list': request.breadcrumb_list} @register.filter def has_permission(request, permission): if permission in request.session.get(settings.PERMISSION_SESSION_KEY): return True
{% extends 'layout.html' %} {% block content %} {% load rbac %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> {% if request|has_permission:'web:customer_add' %} <a class="btn btn-default" href="{% url 'web:customer_add' %}"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} </div> <table class="table table-bordered table-hover"> <thead> <tr> <th>ID</th> <th>客户姓名</th> <th>年龄</th> <th>邮箱</th> <th>公司</th> {% if request|has_permission:'web:customer_edit' or request|has_permission:'web:customer_del' %} {# 自定义filter过滤器 #} <th>选项</th> {% endif %} </tr> </thead> <tbody> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.name }}</td> <td>{{ row.age }}</td> <td>{{ row.email }}</td> <td>{{ row.company }}</td> {% if request|has_permission:'web:customer_edit' or request|has_permission:'web:customer_del' %} <td> {% if request|has_permission:'web:customer_edit' %} <a style="color: #333333;" href="{% url "web:customer_edit" row.id %}"> <i class="fa fa-edit" aria-hidden="true"></i></a> {% endif %} {% if request|has_permission:'web:customer_del' %} <a style="color: #d9534f;" href="{% url "web:customer_del" row.id %}"><i class="fa fa-trash-o"></i></a> {% endif %} </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
第五版:权限管理页面展示与配置
首先注释权限中间件,母版相关权限信息,然后进行开发
""" Django settings for luffy_permission project. Generated by 'django-admin startproject' using Django 1.11.7. For more information on this file, see https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '-t5hehq#zmk=_m)!6pm(c8_s-ycack)$dpppm7ws!&0#eljwzs' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'web.apps.WebConfig', 'rbac.apps.RbacConfig' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 'rbac.middlewares.rbac.PermissionMiddleware', ] ROOT_URLCONF = 'luffy_permission.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'luffy_permission.wsgi.application' # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/' # ###### 权限相关的配置 ###### PERMISSION_SESSION_KEY = 'permissions' MENU_SESSION_KEY = 'menus' WHITE_URL_LIST = [ r'/login/$', r'^/logout/$', r'^/reg/$', r'^/admin/.*', ]
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路飞学城</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} "> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'css/menu.css' %}"> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } </style> {% block css %} {% endblock %} </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">路飞学城 </span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">路飞管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% load rbac %} {# {% menu request %}#} </div> </div> <div class="right-body"> <div> {# {% breadcrumb request %}#} </div> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="{% static 'js/menu.js' %} "></script> {% block js %} {% endblock %} </body> </html>
权限之角色管理,菜单管理
#!/usr/bin/env python # -*- coding:utf-8 -*- from django.utils.safestring import mark_safe import requests from bs4 import BeautifulSoup response = requests.get( url='http://fontawesome.dashgame.com/', ) response.encoding = 'utf-8' soup = BeautifulSoup(response.text, 'html.parser') web = soup.find(attrs={'id': 'web-application'}) icon_list = [] for item in web.find_all(attrs={'class': 'fa-hover'}): tag = item.find('i') class_name = tag.get('class')[1] icon_list.append([class_name, str(tag)]) print(icon_list)
from django.conf.urls import url from rbac import views urlpatterns = [ # /app01/role/list/ # rbac:role_list url(r'^role/list/$', views.role_list, name='role_list'), url(r'^role/add/$', views.role, name='role_add'), url(r'^role/edit/(\d+)$', views.role, name='role_edit'), url(r'^role/del/(\d+)$', views.del_role, name='role_del'), url(r'^menu/list/$', views.menu_list, name='menu_list'), url(r'^menu/add/$', views.menu, name='menu_add'), url(r'^menu/edit/(\d+)$', views.menu, name='menu_edit'), ]
from django.shortcuts import render, HttpResponse, redirect, reverse from rbac import models from rbac.forms import * def role_list(request): all_roles = models.Role.objects.all() return render(request, 'rbac/role_list.html', {"all_roles": all_roles}) def role(request, edit_id=None): obj = models.Role.objects.filter(id=edit_id).first() form_obj = RoleForm(instance=obj) if request.method == 'POST': form_obj = RoleForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:role_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj}) def del_role(request, del_id): models.Role.objects.filter(id=del_id).delete() return redirect(reverse('rbac:role_list')) # 菜单信息 权限信息 def menu_list(request): all_menu = models.Menu.objects.all() all_permission = models.Permission.objects.all() return render(request, 'rbac/menu_list.html', {"all_menu": all_menu, 'all_permission': all_permission}) def menu(request, edit_id=None): obj = models.Menu.objects.filter(id=edit_id).first() form_obj = MenuForm(instance=obj) if request.method == 'POST': form_obj = MenuForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:menu_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj})
from django import forms from rbac import models from django.utils.safestring import mark_safe # 角色的Form class RoleForm(forms.ModelForm): class Meta: model = models.Role fields = ['name'] widgets = { 'name': forms.widgets.Input(attrs={"class": 'form-control'}) } ICON_LIST = [[i[0], mark_safe(i[1])] for i in [ ['fa-address-book', '<i aria-hidden="true" class="fa fa-address-book"></i>'], ['fa-address-book-o', '<i aria-hidden="true" class="fa fa-address-book-o"></i>'], ['fa-address-card', '<i aria-hidden="true" class="fa fa-address-card"></i>'], ['fa-address-card-o', '<i aria-hidden="true" class="fa fa-address-card-o"></i>'], ['fa-adjust', '<i aria-hidden="true" class="fa fa-adjust"></i>'], ['fa-american-sign-language-interpreting', '<i aria-hidden="true" class="fa fa-american-sign-language-interpreting"></i>'], ['fa-anchor', '<i aria-hidden="true" class="fa fa-anchor"></i>'], ['fa-archive', '<i aria-hidden="true" class="fa fa-archive"></i>'], ['fa-area-chart', '<i aria-hidden="true" class="fa fa-area-chart"></i>'], ['fa-arrows', '<i aria-hidden="true" class="fa fa-arrows"></i>'], ['fa-arrows-h', '<i aria-hidden="true" class="fa fa-arrows-h"></i>'], ['fa-arrows-v', '<i aria-hidden="true" class="fa fa-arrows-v"></i>'], ['fa-asl-interpreting', '<i aria-hidden="true" class="fa fa-asl-interpreting"></i>'], ['fa-assistive-listening-systems', '<i aria-hidden="true" class="fa fa-assistive-listening-systems"></i>'], ['fa-asterisk', '<i aria-hidden="true" class="fa fa-asterisk"></i>'], ['fa-at', '<i aria-hidden="true" class="fa fa-at"></i>'], ['fa-audio-description', '<i aria-hidden="true" class="fa fa-audio-description"></i>'], ['fa-automobile', '<i aria-hidden="true" class="fa fa-automobile"></i>'], ['fa-balance-scale', '<i aria-hidden="true" class="fa fa-balance-scale"></i>'], ['fa-ban', '<i aria-hidden="true" class="fa fa-ban"></i>'], ['fa-bank', '<i aria-hidden="true" class="fa fa-bank"></i>'], ['fa-bar-chart', '<i aria-hidden="true" class="fa fa-bar-chart"></i>'], ['fa-bar-chart-o', '<i aria-hidden="true" class="fa fa-bar-chart-o"></i>'], ['fa-barcode', '<i aria-hidden="true" class="fa fa-barcode"></i>'], ['fa-bars', '<i aria-hidden="true" class="fa fa-bars"></i>'], ['fa-bath', '<i aria-hidden="true" class="fa fa-bath"></i>'], ['fa-bathtub', '<i aria-hidden="true" class="fa fa-bathtub"></i>'], ['fa-battery', '<i aria-hidden="true" class="fa fa-battery"></i>'], ['fa-battery-0', '<i aria-hidden="true" class="fa fa-battery-0"></i>'], ['fa-battery-1', '<i aria-hidden="true" class="fa fa-battery-1"></i>'], ['fa-battery-2', '<i aria-hidden="true" class="fa fa-battery-2"></i>'], ['fa-battery-3', '<i aria-hidden="true" class="fa fa-battery-3"></i>'], ['fa-battery-4', '<i aria-hidden="true" class="fa fa-battery-4"></i>'], ['fa-battery-empty', '<i aria-hidden="true" class="fa fa-battery-empty"></i>'], ['fa-battery-full', '<i aria-hidden="true" class="fa fa-battery-full"></i>'], ['fa-battery-half', '<i aria-hidden="true" class="fa fa-battery-half"></i>'], ['fa-battery-quarter', '<i aria-hidden="true" class="fa fa-battery-quarter"></i>'], ['fa-battery-three-quarters', '<i aria-hidden="true" class="fa fa-battery-three-quarters"></i>'], ['fa-bed', '<i aria-hidden="true" class="fa fa-bed"></i>'], ['fa-beer', '<i aria-hidden="true" class="fa fa-beer"></i>'], ['fa-bell', '<i aria-hidden="true" class="fa fa-bell"></i>'], ['fa-bell-o', '<i aria-hidden="true" class="fa fa-bell-o"></i>'], ['fa-bell-slash', '<i aria-hidden="true" class="fa fa-bell-slash"></i>'], ['fa-bell-slash-o', '<i aria-hidden="true" class="fa fa-bell-slash-o"></i>'], ['fa-bicycle', '<i aria-hidden="true" class="fa fa-bicycle"></i>'], ['fa-binoculars', '<i aria-hidden="true" class="fa fa-binoculars"></i>'], ['fa-birthday-cake', '<i aria-hidden="true" class="fa fa-birthday-cake"></i>'], ['fa-blind', '<i aria-hidden="true" class="fa fa-blind"></i>'], ['fa-bluetooth', '<i aria-hidden="true" class="fa fa-bluetooth"></i>'], ['fa-bluetooth-b', '<i aria-hidden="true" class="fa fa-bluetooth-b"></i>'], ['fa-bolt', '<i aria-hidden="true" class="fa fa-bolt"></i>'], ['fa-bomb', '<i aria-hidden="true" class="fa fa-bomb"></i>'], ['fa-book', '<i aria-hidden="true" class="fa fa-book"></i>'], ['fa-bookmark', '<i aria-hidden="true" class="fa fa-bookmark"></i>'], ['fa-bookmark-o', '<i aria-hidden="true" class="fa fa-bookmark-o"></i>'], ['fa-braille', '<i aria-hidden="true" class="fa fa-braille"></i>'], ['fa-briefcase', '<i aria-hidden="true" class="fa fa-briefcase"></i>'], ['fa-bug', '<i aria-hidden="true" class="fa fa-bug"></i>'], ['fa-building', '<i aria-hidden="true" class="fa fa-building"></i>'], ['fa-building-o', '<i aria-hidden="true" class="fa fa-building-o"></i>'], ['fa-bullhorn', '<i aria-hidden="true" class="fa fa-bullhorn"></i>'], ['fa-bullseye', '<i aria-hidden="true" class="fa fa-bullseye"></i>'], ['fa-bus', '<i aria-hidden="true" class="fa fa-bus"></i>'], ['fa-cab', '<i aria-hidden="true" class="fa fa-cab"></i>'], ['fa-calculator', '<i aria-hidden="true" class="fa fa-calculator"></i>'], ['fa-calendar', '<i aria-hidden="true" class="fa fa-calendar"></i>'], ['fa-calendar-check-o', '<i aria-hidden="true" class="fa fa-calendar-check-o"></i>'], ['fa-calendar-minus-o', '<i aria-hidden="true" class="fa fa-calendar-minus-o"></i>'], ['fa-calendar-o', '<i aria-hidden="true" class="fa fa-calendar-o"></i>'], ['fa-calendar-plus-o', '<i aria-hidden="true" class="fa fa-calendar-plus-o"></i>'], ['fa-calendar-times-o', '<i aria-hidden="true" class="fa fa-calendar-times-o"></i>'], ['fa-camera', '<i aria-hidden="true" class="fa fa-camera"></i>'], ['fa-camera-retro', '<i aria-hidden="true" class="fa fa-camera-retro"></i>'], ['fa-car', '<i aria-hidden="true" class="fa fa-car"></i>'], ['fa-caret-square-o-down', '<i aria-hidden="true" class="fa fa-caret-square-o-down"></i>'], ['fa-caret-square-o-left', '<i aria-hidden="true" class="fa fa-caret-square-o-left"></i>'], ['fa-caret-square-o-right', '<i aria-hidden="true" class="fa fa-caret-square-o-right"></i>'], ['fa-caret-square-o-up', '<i aria-hidden="true" class="fa fa-caret-square-o-up"></i>'], ['fa-cart-arrow-down', '<i aria-hidden="true" class="fa fa-cart-arrow-down"></i>'], ['fa-cart-plus', '<i aria-hidden="true" class="fa fa-cart-plus"></i>'], ['fa-cc', '<i aria-hidden="true" class="fa fa-cc"></i>'], ['fa-certificate', '<i aria-hidden="true" class="fa fa-certificate"></i>'], ['fa-check', '<i aria-hidden="true" class="fa fa-check"></i>'], ['fa-check-circle', '<i aria-hidden="true" class="fa fa-check-circle"></i>'], ['fa-check-circle-o', '<i aria-hidden="true" class="fa fa-check-circle-o"></i>'], ['fa-check-square', '<i aria-hidden="true" class="fa fa-check-square"></i>'], ['fa-check-square-o', '<i aria-hidden="true" class="fa fa-check-square-o"></i>'], ['fa-child', '<i aria-hidden="true" class="fa fa-child"></i>'], ['fa-circle', '<i aria-hidden="true" class="fa fa-circle"></i>'], ['fa-circle-o', '<i aria-hidden="true" class="fa fa-circle-o"></i>'], ['fa-circle-o-notch', '<i aria-hidden="true" class="fa fa-circle-o-notch"></i>'], ['fa-circle-thin', '<i aria-hidden="true" class="fa fa-circle-thin"></i>'], ['fa-clock-o', '<i aria-hidden="true" class="fa fa-clock-o"></i>'], ['fa-clone', '<i aria-hidden="true" class="fa fa-clone"></i>'], ['fa-close', '<i aria-hidden="true" class="fa fa-close"></i>'], ['fa-cloud', '<i aria-hidden="true" class="fa fa-cloud"></i>'], ['fa-cloud-download', '<i aria-hidden="true" class="fa fa-cloud-download"></i>'], ['fa-cloud-upload', '<i aria-hidden="true" class="fa fa-cloud-upload"></i>'], ['fa-code', '<i aria-hidden="true" class="fa fa-code"></i>'], ['fa-code-fork', '<i aria-hidden="true" class="fa fa-code-fork"></i>'], ['fa-coffee', '<i aria-hidden="true" class="fa fa-coffee"></i>'], ['fa-cog', '<i aria-hidden="true" class="fa fa-cog"></i>'], ['fa-cogs', '<i aria-hidden="true" class="fa fa-cogs"></i>'], ['fa-comment', '<i aria-hidden="true" class="fa fa-comment"></i>'], ['fa-comment-o', '<i aria-hidden="true" class="fa fa-comment-o"></i>'], ['fa-commenting', '<i aria-hidden="true" class="fa fa-commenting"></i>'], ['fa-commenting-o', '<i aria-hidden="true" class="fa fa-commenting-o"></i>'], ['fa-comments', '<i aria-hidden="true" class="fa fa-comments"></i>'], ['fa-comments-o', '<i aria-hidden="true" class="fa fa-comments-o"></i>'], ['fa-compass', '<i aria-hidden="true" class="fa fa-compass"></i>'], ['fa-copyright', '<i aria-hidden="true" class="fa fa-copyright"></i>'], ['fa-creative-commons', '<i aria-hidden="true" class="fa fa-creative-commons"></i>'], ['fa-credit-card', '<i aria-hidden="true" class="fa fa-credit-card"></i>'], ['fa-credit-card-alt', '<i aria-hidden="true" class="fa fa-credit-card-alt"></i>'], ['fa-crop', '<i aria-hidden="true" class="fa fa-crop"></i>'], ['fa-crosshairs', '<i aria-hidden="true" class="fa fa-crosshairs"></i>'], ['fa-cube', '<i aria-hidden="true" class="fa fa-cube"></i>'], ['fa-cubes', '<i aria-hidden="true" class="fa fa-cubes"></i>'], ['fa-cutlery', '<i aria-hidden="true" class="fa fa-cutlery"></i>'], ['fa-dashboard', '<i aria-hidden="true" class="fa fa-dashboard"></i>'], ['fa-database', '<i aria-hidden="true" class="fa fa-database"></i>'], ['fa-deaf', '<i aria-hidden="true" class="fa fa-deaf"></i>'], ['fa-deafness', '<i aria-hidden="true" class="fa fa-deafness"></i>'], ['fa-desktop', '<i aria-hidden="true" class="fa fa-desktop"></i>'], ['fa-diamond', '<i aria-hidden="true" class="fa fa-diamond"></i>'], ['fa-dot-circle-o', '<i aria-hidden="true" class="fa fa-dot-circle-o"></i>'], ['fa-download', '<i aria-hidden="true" class="fa fa-download"></i>'], ['fa-drivers-license', '<i aria-hidden="true" class="fa fa-drivers-license"></i>'], ['fa-drivers-license-o', '<i aria-hidden="true" class="fa fa-drivers-license-o"></i>'], ['fa-edit', '<i aria-hidden="true" class="fa fa-edit"></i>'], ['fa-ellipsis-h', '<i aria-hidden="true" class="fa fa-ellipsis-h"></i>'], ['fa-ellipsis-v', '<i aria-hidden="true" class="fa fa-ellipsis-v"></i>'], ['fa-envelope', '<i aria-hidden="true" class="fa fa-envelope"></i>'], ['fa-envelope-o', '<i aria-hidden="true" class="fa fa-envelope-o"></i>'], ['fa-envelope-open', '<i aria-hidden="true" class="fa fa-envelope-open"></i>'], ['fa-envelope-open-o', '<i aria-hidden="true" class="fa fa-envelope-open-o"></i>'], ['fa-envelope-square', '<i aria-hidden="true" class="fa fa-envelope-square"></i>'], ['fa-eraser', '<i aria-hidden="true" class="fa fa-eraser"></i>'], ['fa-exchange', '<i aria-hidden="true" class="fa fa-exchange"></i>'], ['fa-exclamation', '<i aria-hidden="true" class="fa fa-exclamation"></i>'], ['fa-exclamation-circle', '<i aria-hidden="true" class="fa fa-exclamation-circle"></i>'], ['fa-exclamation-triangle', '<i aria-hidden="true" class="fa fa-exclamation-triangle"></i>'], ['fa-external-link', '<i aria-hidden="true" class="fa fa-external-link"></i>'], ['fa-external-link-square', '<i aria-hidden="true" class="fa fa-external-link-square"></i>'], ['fa-eye', '<i aria-hidden="true" class="fa fa-eye"></i>'], ['fa-eye-slash', '<i aria-hidden="true" class="fa fa-eye-slash"></i>'], ['fa-eyedropper', '<i aria-hidden="true" class="fa fa-eyedropper"></i>'], ['fa-fax', '<i aria-hidden="true" class="fa fa-fax"></i>'], ['fa-feed', '<i aria-hidden="true" class="fa fa-feed"></i>'], ['fa-female', '<i aria-hidden="true" class="fa fa-female"></i>'], ['fa-fighter-jet', '<i aria-hidden="true" class="fa fa-fighter-jet"></i>'], ['fa-file-archive-o', '<i aria-hidden="true" class="fa fa-file-archive-o"></i>'], ['fa-file-audio-o', '<i aria-hidden="true" class="fa fa-file-audio-o"></i>'], ['fa-file-code-o', '<i aria-hidden="true" class="fa fa-file-code-o"></i>'], ['fa-file-excel-o', '<i aria-hidden="true" class="fa fa-file-excel-o"></i>'], ['fa-file-image-o', '<i aria-hidden="true" class="fa fa-file-image-o"></i>'], ['fa-file-movie-o', '<i aria-hidden="true" class="fa fa-file-movie-o"></i>'], ['fa-file-pdf-o', '<i aria-hidden="true" class="fa fa-file-pdf-o"></i>'], ['fa-file-photo-o', '<i aria-hidden="true" class="fa fa-file-photo-o"></i>'], ['fa-file-picture-o', '<i aria-hidden="true" class="fa fa-file-picture-o"></i>'], ['fa-file-powerpoint-o', '<i aria-hidden="true" class="fa fa-file-powerpoint-o"></i>'], ['fa-file-sound-o', '<i aria-hidden="true" class="fa fa-file-sound-o"></i>'], ['fa-file-video-o', '<i aria-hidden="true" class="fa fa-file-video-o"></i>'], ['fa-file-word-o', '<i aria-hidden="true" class="fa fa-file-word-o"></i>'], ['fa-file-zip-o', '<i aria-hidden="true" class="fa fa-file-zip-o"></i>'], ['fa-film', '<i aria-hidden="true" class="fa fa-film"></i>'], ['fa-filter', '<i aria-hidden="true" class="fa fa-filter"></i>'], ['fa-fire', '<i aria-hidden="true" class="fa fa-fire"></i>'], ['fa-fire-extinguisher', '<i aria-hidden="true" class="fa fa-fire-extinguisher"></i>'], ['fa-flag', '<i aria-hidden="true" class="fa fa-flag"></i>'], ['fa-flag-checkered', '<i aria-hidden="true" class="fa fa-flag-checkered"></i>'], ['fa-flag-o', '<i aria-hidden="true" class="fa fa-flag-o"></i>'], ['fa-flash', '<i aria-hidden="true" class="fa fa-flash"></i>'], ['fa-flask', '<i aria-hidden="true" class="fa fa-flask"></i>'], ['fa-folder', '<i aria-hidden="true" class="fa fa-folder"></i>'], ['fa-folder-o', '<i aria-hidden="true" class="fa fa-folder-o"></i>'], ['fa-folder-open', '<i aria-hidden="true" class="fa fa-folder-open"></i>'], ['fa-folder-open-o', '<i aria-hidden="true" class="fa fa-folder-open-o"></i>'], ['fa-frown-o', '<i aria-hidden="true" class="fa fa-frown-o"></i>'], ['fa-futbol-o', '<i aria-hidden="true" class="fa fa-futbol-o"></i>'], ['fa-gamepad', '<i aria-hidden="true" class="fa fa-gamepad"></i>'], ['fa-gavel', '<i aria-hidden="true" class="fa fa-gavel"></i>'], ['fa-gear', '<i aria-hidden="true" class="fa fa-gear"></i>'], ['fa-gears', '<i aria-hidden="true" class="fa fa-gears"></i>'], ['fa-gift', '<i aria-hidden="true" class="fa fa-gift"></i>'], ['fa-glass', '<i aria-hidden="true" class="fa fa-glass"></i>'], ['fa-globe', '<i aria-hidden="true" class="fa fa-globe"></i>'], ['fa-graduation-cap', '<i aria-hidden="true" class="fa fa-graduation-cap"></i>'], ['fa-group', '<i aria-hidden="true" class="fa fa-group"></i>'], ['fa-hand-grab-o', '<i aria-hidden="true" class="fa fa-hand-grab-o"></i>'], ['fa-hand-lizard-o', '<i aria-hidden="true" class="fa fa-hand-lizard-o"></i>'], ['fa-hand-paper-o', '<i aria-hidden="true" class="fa fa-hand-paper-o"></i>'], ['fa-hand-peace-o', '<i aria-hidden="true" class="fa fa-hand-peace-o"></i>'], ['fa-hand-pointer-o', '<i aria-hidden="true" class="fa fa-hand-pointer-o"></i>'], ['fa-hand-rock-o', '<i aria-hidden="true" class="fa fa-hand-rock-o"></i>'], ['fa-hand-scissors-o', '<i aria-hidden="true" class="fa fa-hand-scissors-o"></i>'], ['fa-hand-spock-o', '<i aria-hidden="true" class="fa fa-hand-spock-o"></i>'], ['fa-hand-stop-o', '<i aria-hidden="true" class="fa fa-hand-stop-o"></i>'], ['fa-handshake-o', '<i aria-hidden="true" class="fa fa-handshake-o"></i>'], ['fa-hard-of-hearing', '<i aria-hidden="true" class="fa fa-hard-of-hearing"></i>'], ['fa-hashtag', '<i aria-hidden="true" class="fa fa-hashtag"></i>'], ['fa-hdd-o', '<i aria-hidden="true" class="fa fa-hdd-o"></i>'], ['fa-headphones', '<i aria-hidden="true" class="fa fa-headphones"></i>'], ['fa-heart', '<i aria-hidden="true" class="fa fa-heart"></i>'], ['fa-heart-o', '<i aria-hidden="true" class="fa fa-heart-o"></i>'], ['fa-heartbeat', '<i aria-hidden="true" class="fa fa-heartbeat"></i>'], ['fa-history', '<i aria-hidden="true" class="fa fa-history"></i>'], ['fa-home', '<i aria-hidden="true" class="fa fa-home"></i>'], ['fa-hotel', '<i aria-hidden="true" class="fa fa-hotel"></i>'], ['fa-hourglass', '<i aria-hidden="true" class="fa fa-hourglass"></i>'], ['fa-hourglass-1', '<i aria-hidden="true" class="fa fa-hourglass-1"></i>'], ['fa-hourglass-2', '<i aria-hidden="true" class="fa fa-hourglass-2"></i>'], ['fa-hourglass-3', '<i aria-hidden="true" class="fa fa-hourglass-3"></i>'], ['fa-hourglass-end', '<i aria-hidden="true" class="fa fa-hourglass-end"></i>'], ['fa-hourglass-half', '<i aria-hidden="true" class="fa fa-hourglass-half"></i>'], ['fa-hourglass-o', '<i aria-hidden="true" class="fa fa-hourglass-o"></i>'], ['fa-hourglass-start', '<i aria-hidden="true" class="fa fa-hourglass-start"></i>'], ['fa-i-cursor', '<i aria-hidden="true" class="fa fa-i-cursor"></i>'], ['fa-id-badge', '<i aria-hidden="true" class="fa fa-id-badge"></i>'], ['fa-id-card', '<i aria-hidden="true" class="fa fa-id-card"></i>'], ['fa-id-card-o', '<i aria-hidden="true" class="fa fa-id-card-o"></i>'], ['fa-image', '<i aria-hidden="true" class="fa fa-image"></i>'], ['fa-inbox', '<i aria-hidden="true" class="fa fa-inbox"></i>'], ['fa-industry', '<i aria-hidden="true" class="fa fa-industry"></i>'], ['fa-info', '<i aria-hidden="true" class="fa fa-info"></i>'], ['fa-info-circle', '<i aria-hidden="true" class="fa fa-info-circle"></i>'], ['fa-institution', '<i aria-hidden="true" class="fa fa-institution"></i>'], ['fa-key', '<i aria-hidden="true" class="fa fa-key"></i>'], ['fa-keyboard-o', '<i aria-hidden="true" class="fa fa-keyboard-o"></i>'], ['fa-language', '<i aria-hidden="true" class="fa fa-language"></i>'], ['fa-laptop', '<i aria-hidden="true" class="fa fa-laptop"></i>'], ['fa-leaf', '<i aria-hidden="true" class="fa fa-leaf"></i>'], ['fa-legal', '<i aria-hidden="true" class="fa fa-legal"></i>'], ['fa-lemon-o', '<i aria-hidden="true" class="fa fa-lemon-o"></i>'], ['fa-level-down', '<i aria-hidden="true" class="fa fa-level-down"></i>'], ['fa-level-up', '<i aria-hidden="true" class="fa fa-level-up"></i>'], ['fa-life-bouy', '<i aria-hidden="true" class="fa fa-life-bouy"></i>'], ['fa-life-buoy', '<i aria-hidden="true" class="fa fa-life-buoy"></i>'], ['fa-life-ring', '<i aria-hidden="true" class="fa fa-life-ring"></i>'], ['fa-life-saver', '<i aria-hidden="true" class="fa fa-life-saver"></i>'], ['fa-lightbulb-o', '<i aria-hidden="true" class="fa fa-lightbulb-o"></i>'], ['fa-line-chart', '<i aria-hidden="true" class="fa fa-line-chart"></i>'], ['fa-location-arrow', '<i aria-hidden="true" class="fa fa-location-arrow"></i>'], ['fa-lock', '<i aria-hidden="true" class="fa fa-lock"></i>'], ['fa-low-vision', '<i aria-hidden="true" class="fa fa-low-vision"></i>'], ['fa-magic', '<i aria-hidden="true" class="fa fa-magic"></i>'], ['fa-magnet', '<i aria-hidden="true" class="fa fa-magnet"></i>'], ['fa-mail-forward', '<i aria-hidden="true" class="fa fa-mail-forward"></i>'], ['fa-mail-reply', '<i aria-hidden="true" class="fa fa-mail-reply"></i>'], ['fa-mail-reply-all', '<i aria-hidden="true" class="fa fa-mail-reply-all"></i>'], ['fa-male', '<i aria-hidden="true" class="fa fa-male"></i>'], ['fa-map', '<i aria-hidden="true" class="fa fa-map"></i>'], ['fa-map-marker', '<i aria-hidden="true" class="fa fa-map-marker"></i>'], ['fa-map-o', '<i aria-hidden="true" class="fa fa-map-o"></i>'], ['fa-map-pin', '<i aria-hidden="true" class="fa fa-map-pin"></i>'], ['fa-map-signs', '<i aria-hidden="true" class="fa fa-map-signs"></i>'], ['fa-meh-o', '<i aria-hidden="true" class="fa fa-meh-o"></i>'], ['fa-microchip', '<i aria-hidden="true" class="fa fa-microchip"></i>'], ['fa-microphone', '<i aria-hidden="true" class="fa fa-microphone"></i>'], ['fa-microphone-slash', '<i aria-hidden="true" class="fa fa-microphone-slash"></i>'], ['fa-minus', '<i aria-hidden="true" class="fa fa-minus"></i>'], ['fa-minus-circle', '<i aria-hidden="true" class="fa fa-minus-circle"></i>'], ['fa-minus-square', '<i aria-hidden="true" class="fa fa-minus-square"></i>'], ['fa-minus-square-o', '<i aria-hidden="true" class="fa fa-minus-square-o"></i>'], ['fa-mobile', '<i aria-hidden="true" class="fa fa-mobile"></i>'], ['fa-mobile-phone', '<i aria-hidden="true" class="fa fa-mobile-phone"></i>'], ['fa-money', '<i aria-hidden="true" class="fa fa-money"></i>'], ['fa-moon-o', '<i aria-hidden="true" class="fa fa-moon-o"></i>'], ['fa-mortar-board', '<i aria-hidden="true" class="fa fa-mortar-board"></i>'], ['fa-motorcycle', '<i aria-hidden="true" class="fa fa-motorcycle"></i>'], ['fa-mouse-pointer', '<i aria-hidden="true" class="fa fa-mouse-pointer"></i>'], ['fa-music', '<i aria-hidden="true" class="fa fa-music"></i>'], ['fa-navicon', '<i aria-hidden="true" class="fa fa-navicon"></i>'], ['fa-newspaper-o', '<i aria-hidden="true" class="fa fa-newspaper-o"></i>'], ['fa-object-group', '<i aria-hidden="true" class="fa fa-object-group"></i>'], ['fa-object-ungroup', '<i aria-hidden="true" class="fa fa-object-ungroup"></i>'], ['fa-paint-brush', '<i aria-hidden="true" class="fa fa-paint-brush"></i>'], ['fa-paper-plane', '<i aria-hidden="true" class="fa fa-paper-plane"></i>'], ['fa-paper-plane-o', '<i aria-hidden="true" class="fa fa-paper-plane-o"></i>'], ['fa-paw', '<i aria-hidden="true" class="fa fa-paw"></i>'], ['fa-pencil', '<i aria-hidden="true" class="fa fa-pencil"></i>'], ['fa-pencil-square', '<i aria-hidden="true" class="fa fa-pencil-square"></i>'], ['fa-pencil-square-o', '<i aria-hidden="true" class="fa fa-pencil-square-o"></i>'], ['fa-percent', '<i aria-hidden="true" class="fa fa-percent"></i>'], ['fa-phone', '<i aria-hidden="true" class="fa fa-phone"></i>'], ['fa-phone-square', '<i aria-hidden="true" class="fa fa-phone-square"></i>'], ['fa-photo', '<i aria-hidden="true" class="fa fa-photo"></i>'], ['fa-picture-o', '<i aria-hidden="true" class="fa fa-picture-o"></i>'], ['fa-pie-chart', '<i aria-hidden="true" class="fa fa-pie-chart"></i>'], ['fa-plane', '<i aria-hidden="true" class="fa fa-plane"></i>'], ['fa-plug', '<i aria-hidden="true" class="fa fa-plug"></i>'], ['fa-plus', '<i aria-hidden="true" class="fa fa-plus"></i>'], ['fa-plus-circle', '<i aria-hidden="true" class="fa fa-plus-circle"></i>'], ['fa-plus-square', '<i aria-hidden="true" class="fa fa-plus-square"></i>'], ['fa-plus-square-o', '<i aria-hidden="true" class="fa fa-plus-square-o"></i>'], ['fa-podcast', '<i aria-hidden="true" class="fa fa-podcast"></i>'], ['fa-power-off', '<i aria-hidden="true" class="fa fa-power-off"></i>'], ['fa-print', '<i aria-hidden="true" class="fa fa-print"></i>'], ['fa-puzzle-piece', '<i aria-hidden="true" class="fa fa-puzzle-piece"></i>'], ['fa-qrcode', '<i aria-hidden="true" class="fa fa-qrcode"></i>'], ['fa-question', '<i aria-hidden="true" class="fa fa-question"></i>'], ['fa-question-circle', '<i aria-hidden="true" class="fa fa-question-circle"></i>'], ['fa-question-circle-o', '<i aria-hidden="true" class="fa fa-question-circle-o"></i>'], ['fa-quote-left', '<i aria-hidden="true" class="fa fa-quote-left"></i>'], ['fa-quote-right', '<i aria-hidden="true" class="fa fa-quote-right"></i>'], ['fa-random', '<i aria-hidden="true" class="fa fa-random"></i>'], ['fa-recycle', '<i aria-hidden="true" class="fa fa-recycle"></i>'], ['fa-refresh', '<i aria-hidden="true" class="fa fa-refresh"></i>'], ['fa-registered', '<i aria-hidden="true" class="fa fa-registered"></i>'], ['fa-remove', '<i aria-hidden="true" class="fa fa-remove"></i>'], ['fa-reorder', '<i aria-hidden="true" class="fa fa-reorder"></i>'], ['fa-reply', '<i aria-hidden="true" class="fa fa-reply"></i>'], ['fa-reply-all', '<i aria-hidden="true" class="fa fa-reply-all"></i>'], ['fa-retweet', '<i aria-hidden="true" class="fa fa-retweet"></i>'], ['fa-road', '<i aria-hidden="true" class="fa fa-road"></i>'], ['fa-rocket', '<i aria-hidden="true" class="fa fa-rocket"></i>'], ['fa-rss', '<i aria-hidden="true" class="fa fa-rss"></i>'], ['fa-rss-square', '<i aria-hidden="true" class="fa fa-rss-square"></i>'], ['fa-s15', '<i aria-hidden="true" class="fa fa-s15"></i>'], ['fa-search', '<i aria-hidden="true" class="fa fa-search"></i>'], ['fa-search-minus', '<i aria-hidden="true" class="fa fa-search-minus"></i>'], ['fa-search-plus', '<i aria-hidden="true" class="fa fa-search-plus"></i>'], ['fa-send', '<i aria-hidden="true" class="fa fa-send"></i>'], ['fa-send-o', '<i aria-hidden="true" class="fa fa-send-o"></i>'], ['fa-server', '<i aria-hidden="true" class="fa fa-server"></i>'], ['fa-share', '<i aria-hidden="true" class="fa fa-share"></i>'], ['fa-share-alt', '<i aria-hidden="true" class="fa fa-share-alt"></i>'], ['fa-share-alt-square', '<i aria-hidden="true" class="fa fa-share-alt-square"></i>'], ['fa-share-square', '<i aria-hidden="true" class="fa fa-share-square"></i>'], ['fa-share-square-o', '<i aria-hidden="true" class="fa fa-share-square-o"></i>'], ['fa-shield', '<i aria-hidden="true" class="fa fa-shield"></i>'], ['fa-ship', '<i aria-hidden="true" class="fa fa-ship"></i>'], ['fa-shopping-bag', '<i aria-hidden="true" class="fa fa-shopping-bag"></i>'], ['fa-shopping-basket', '<i aria-hidden="true" class="fa fa-shopping-basket"></i>'], ['fa-shopping-cart', '<i aria-hidden="true" class="fa fa-shopping-cart"></i>'], ['fa-shower', '<i aria-hidden="true" class="fa fa-shower"></i>'], ['fa-sign-in', '<i aria-hidden="true" class="fa fa-sign-in"></i>'], ['fa-sign-language', '<i aria-hidden="true" class="fa fa-sign-language"></i>'], ['fa-sign-out', '<i aria-hidden="true" class="fa fa-sign-out"></i>'], ['fa-signal', '<i aria-hidden="true" class="fa fa-signal"></i>'], ['fa-signing', '<i aria-hidden="true" class="fa fa-signing"></i>'], ['fa-sitemap', '<i aria-hidden="true" class="fa fa-sitemap"></i>'], ['fa-sliders', '<i aria-hidden="true" class="fa fa-sliders"></i>'], ['fa-smile-o', '<i aria-hidden="true" class="fa fa-smile-o"></i>'], ['fa-snowflake-o', '<i aria-hidden="true" class="fa fa-snowflake-o"></i>'], ['fa-soccer-ball-o', '<i aria-hidden="true" class="fa fa-soccer-ball-o"></i>'], ['fa-sort', '<i aria-hidden="true" class="fa fa-sort"></i>'], ['fa-sort-alpha-asc', '<i aria-hidden="true" class="fa fa-sort-alpha-asc"></i>'], ['fa-sort-alpha-desc', '<i aria-hidden="true" class="fa fa-sort-alpha-desc"></i>'], ['fa-sort-amount-asc', '<i aria-hidden="true" class="fa fa-sort-amount-asc"></i>'], ['fa-sort-amount-desc', '<i aria-hidden="true" class="fa fa-sort-amount-desc"></i>'], ['fa-sort-asc', '<i aria-hidden="true" class="fa fa-sort-asc"></i>'], ['fa-sort-desc', '<i aria-hidden="true" class="fa fa-sort-desc"></i>'], ['fa-sort-down', '<i aria-hidden="true" class="fa fa-sort-down"></i>'], ['fa-sort-numeric-asc', '<i aria-hidden="true" class="fa fa-sort-numeric-asc"></i>'], ['fa-sort-numeric-desc', '<i aria-hidden="true" class="fa fa-sort-numeric-desc"></i>'], ['fa-sort-up', '<i aria-hidden="true" class="fa fa-sort-up"></i>'], ['fa-space-shuttle', '<i aria-hidden="true" class="fa fa-space-shuttle"></i>'], ['fa-spinner', '<i aria-hidden="true" class="fa fa-spinner"></i>'], ['fa-spoon', '<i aria-hidden="true" class="fa fa-spoon"></i>'], ['fa-square', '<i aria-hidden="true" class="fa fa-square"></i>'], ['fa-square-o', '<i aria-hidden="true" class="fa fa-square-o"></i>'], ['fa-star', '<i aria-hidden="true" class="fa fa-star"></i>'], ['fa-star-half', '<i aria-hidden="true" class="fa fa-star-half"></i>'], ['fa-star-half-empty', '<i aria-hidden="true" class="fa fa-star-half-empty"></i>'], ['fa-star-half-full', '<i aria-hidden="true" class="fa fa-star-half-full"></i>'], ['fa-star-half-o', '<i aria-hidden="true" class="fa fa-star-half-o"></i>'], ['fa-star-o', '<i aria-hidden="true" class="fa fa-star-o"></i>'], ['fa-sticky-note', '<i aria-hidden="true" class="fa fa-sticky-note"></i>'], ['fa-sticky-note-o', '<i aria-hidden="true" class="fa fa-sticky-note-o"></i>'], ['fa-street-view', '<i aria-hidden="true" class="fa fa-street-view"></i>'], ['fa-suitcase', '<i aria-hidden="true" class="fa fa-suitcase"></i>'], ['fa-sun-o', '<i aria-hidden="true" class="fa fa-sun-o"></i>'], ['fa-support', '<i aria-hidden="true" class="fa fa-support"></i>'], ['fa-tablet', '<i aria-hidden="true" class="fa fa-tablet"></i>'], ['fa-tachometer', '<i aria-hidden="true" class="fa fa-tachometer"></i>'], ['fa-tag', '<i aria-hidden="true" class="fa fa-tag"></i>'], ['fa-tags', '<i aria-hidden="true" class="fa fa-tags"></i>'], ['fa-tasks', '<i aria-hidden="true" class="fa fa-tasks"></i>'], ['fa-taxi', '<i aria-hidden="true" class="fa fa-taxi"></i>'], ['fa-television', '<i aria-hidden="true" class="fa fa-television"></i>'], ['fa-terminal', '<i aria-hidden="true" class="fa fa-terminal"></i>'], ['fa-thermometer', '<i aria-hidden="true" class="fa fa-thermometer"></i>'], ['fa-thermometer-0', '<i aria-hidden="true" class="fa fa-thermometer-0"></i>'], ['fa-thermometer-1', '<i aria-hidden="true" class="fa fa-thermometer-1"></i>'], ['fa-thermometer-2', '<i aria-hidden="true" class="fa fa-thermometer-2"></i>'], ['fa-thermometer-3', '<i aria-hidden="true" class="fa fa-thermometer-3"></i>'], ['fa-thermometer-4', '<i aria-hidden="true" class="fa fa-thermometer-4"></i>'], ['fa-thermometer-empty', '<i aria-hidden="true" class="fa fa-thermometer-empty"></i>'], ['fa-thermometer-full', '<i aria-hidden="true" class="fa fa-thermometer-full"></i>'], ['fa-thermometer-half', '<i aria-hidden="true" class="fa fa-thermometer-half"></i>'], ['fa-thermometer-quarter', '<i aria-hidden="true" class="fa fa-thermometer-quarter"></i>'], ['fa-thermometer-three-quarters', '<i aria-hidden="true" class="fa fa-thermometer-three-quarters"></i>'], ['fa-thumb-tack', '<i aria-hidden="true" class="fa fa-thumb-tack"></i>'], ['fa-thumbs-down', '<i aria-hidden="true" class="fa fa-thumbs-down"></i>'], ['fa-thumbs-o-down', '<i aria-hidden="true" class="fa fa-thumbs-o-down"></i>'], ['fa-thumbs-o-up', '<i aria-hidden="true" class="fa fa-thumbs-o-up"></i>'], ['fa-thumbs-up', '<i aria-hidden="true" class="fa fa-thumbs-up"></i>'], ['fa-ticket', '<i aria-hidden="true" class="fa fa-ticket"></i>'], ['fa-times', '<i aria-hidden="true" class="fa fa-times"></i>'], ['fa-times-circle', '<i aria-hidden="true" class="fa fa-times-circle"></i>'], ['fa-times-circle-o', '<i aria-hidden="true" class="fa fa-times-circle-o"></i>'], ['fa-times-rectangle', '<i aria-hidden="true" class="fa fa-times-rectangle"></i>'], ['fa-times-rectangle-o', '<i aria-hidden="true" class="fa fa-times-rectangle-o"></i>'], ['fa-tint', '<i aria-hidden="true" class="fa fa-tint"></i>'], ['fa-toggle-down', '<i aria-hidden="true" class="fa fa-toggle-down"></i>'], ['fa-toggle-left', '<i aria-hidden="true" class="fa fa-toggle-left"></i>'], ['fa-toggle-off', '<i aria-hidden="true" class="fa fa-toggle-off"></i>'], ['fa-toggle-on', '<i aria-hidden="true" class="fa fa-toggle-on"></i>'], ['fa-toggle-right', '<i aria-hidden="true" class="fa fa-toggle-right"></i>'], ['fa-toggle-up', '<i aria-hidden="true" class="fa fa-toggle-up"></i>'], ['fa-trademark', '<i aria-hidden="true" class="fa fa-trademark"></i>'], ['fa-trash', '<i aria-hidden="true" class="fa fa-trash"></i>'], ['fa-trash-o', '<i aria-hidden="true" class="fa fa-trash-o"></i>'], ['fa-tree', '<i aria-hidden="true" class="fa fa-tree"></i>'], ['fa-trophy', '<i aria-hidden="true" class="fa fa-trophy"></i>'], ['fa-truck', '<i aria-hidden="true" class="fa fa-truck"></i>'], ['fa-tty', '<i aria-hidden="true" class="fa fa-tty"></i>'], ['fa-tv', '<i aria-hidden="true" class="fa fa-tv"></i>'], ['fa-umbrella', '<i aria-hidden="true" class="fa fa-umbrella"></i>'], ['fa-universal-access', '<i aria-hidden="true" class="fa fa-universal-access"></i>'], ['fa-university', '<i aria-hidden="true" class="fa fa-university"></i>'], ['fa-unlock', '<i aria-hidden="true" class="fa fa-unlock"></i>'], ['fa-unlock-alt', '<i aria-hidden="true" class="fa fa-unlock-alt"></i>'], ['fa-unsorted', '<i aria-hidden="true" class="fa fa-unsorted"></i>'], ['fa-upload', '<i aria-hidden="true" class="fa fa-upload"></i>'], ['fa-user', '<i aria-hidden="true" class="fa fa-user"></i>'], ['fa-user-circle', '<i aria-hidden="true" class="fa fa-user-circle"></i>'], ['fa-user-circle-o', '<i aria-hidden="true" class="fa fa-user-circle-o"></i>'], ['fa-user-o', '<i aria-hidden="true" class="fa fa-user-o"></i>'], ['fa-user-plus', '<i aria-hidden="true" class="fa fa-user-plus"></i>'], ['fa-user-secret', '<i aria-hidden="true" class="fa fa-user-secret"></i>'], ['fa-user-times', '<i aria-hidden="true" class="fa fa-user-times"></i>'], ['fa-users', '<i aria-hidden="true" class="fa fa-users"></i>'], ['fa-vcard', '<i aria-hidden="true" class="fa fa-vcard"></i>'], ['fa-vcard-o', '<i aria-hidden="true" class="fa fa-vcard-o"></i>'], ['fa-video-camera', '<i aria-hidden="true" class="fa fa-video-camera"></i>'], ['fa-volume-control-phone', '<i aria-hidden="true" class="fa fa-volume-control-phone"></i>'], ['fa-volume-down', '<i aria-hidden="true" class="fa fa-volume-down"></i>'], ['fa-volume-off', '<i aria-hidden="true" class="fa fa-volume-off"></i>'], ['fa-volume-up', '<i aria-hidden="true" class="fa fa-volume-up"></i>'], ['fa-warning', '<i aria-hidden="true" class="fa fa-warning"></i>'], ['fa-wheelchair', '<i aria-hidden="true" class="fa fa-wheelchair"></i>'], ['fa-wheelchair-alt', '<i aria-hidden="true" class="fa fa-wheelchair-alt"></i>'], ['fa-wifi', '<i aria-hidden="true" class="fa fa-wifi"></i>'], ['fa-window-close', '<i aria-hidden="true" class="fa fa-window-close"></i>'], ['fa-window-close-o', '<i aria-hidden="true" class="fa fa-window-close-o"></i>'], ['fa-window-maximize', '<i aria-hidden="true" class="fa fa-window-maximize"></i>'], ['fa-window-minimize', '<i aria-hidden="true" class="fa fa-window-minimize"></i>'], ['fa-window-restore', '<i aria-hidden="true" class="fa fa-window-restore"></i>'], ['fa-wrench', '<i aria-hidden="true" class="fa fa-wrench"></i>'] ]] # 菜单的Form class MenuForm(forms.ModelForm): class Meta: model = models.Menu fields = ['title', 'weight', 'icon', ] widgets = { 'title': forms.widgets.Input(attrs={"class": 'form-control'}), 'weight': forms.widgets.Input(attrs={"class": 'form-control'}), 'icon': forms.widgets.RadioSelect(choices=ICON_LIST), }
templates
{% extends 'layout.html' %} {% block content %} <div style="margin: 20px"> <h1>角色管理</h1> <a href="{% url "rbac:role_add" %}" class="btn btn-success">添加</a> <table class="table table-bordered table-hover" style="margin-top: 5px"> <thead> <tr> <th>序号</th> <th>名称</th> <th>操作</th> </tr> </thead> <tbody> {% for role in all_roles %} <tr> <td>{{ forloop.counter }}</td> <td>{{ role.name }}</td> <td> <a href="{% url 'rbac:role_edit' role.id %}"> <i class="fa fa-edit"></i> </a> <a href="{% url 'rbac:role_del' role.id %}"> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div style="margin: 20px"> <div class="col-sm-3"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><i class="fa fa-book"></i> 菜单管理 <a href="{% url 'rbac:menu_add' %}" class="btn btn-success btn-sm pull-right " style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-plus"></i> 新建</a> </div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>图标</th> <th>操作</th> </tr> </thead> <tbody> {% for menu in all_menu %} <tr> <td>{{ menu.title }}</td> <td> <i class="fa {{ menu.icon }} "></i> </td> <td> <a href="{% url 'rbac:menu_edit' menu.id %}"> <i class="fa fa-edit"></i> </a> <a href=""> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> </div> <div class="col-sm-9"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><i class="fa fa-cubes"></i> 权限管理</div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>URL</th> <th>URL别名</th> <th>操作</th> </tr> </thead> <tbody> {% for permission in all_permission %} <tr> <td>{{ permission.title }}</td> <td>{{ permission.url }}</td> <td>{{ permission.name }}</td> <td> <a href=""> <i class="fa fa-edit"></i> </a> <a href=""> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> {% endblock %}
{% extends 'layout.html' %} {% block css %} <style> ul { list-style-type: none; padding: 0; } ul li { float: left; padding: 10px; padding-left: 0; width: 80px; } ul li i { font-size: 18px; margin-left: 5px; color: #6d6565; } </style> {% endblock %} {% block content %} <form class="form-horizontal" novalidate method="post" action="" style="margin-top: 50px"> {% csrf_token %} {% for field in form_obj %} <div class="form-group {% if field.errors %}has-error{% endif %} "> <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label> <div class="col-sm-6"> {{ field }} </div> <span class="help-block">{{ field.errors.0 }}</span> </div> {% endfor %} <div class="form-group"> <div class="col-sm-offset-2 col-sm-6"> <button type="submit" class="btn btn-default">提交</button> </div> </div> </form> {% endblock %}
权限信息展示
from django.shortcuts import render, HttpResponse, redirect, reverse from rbac import models from rbac.forms import * from django.db.models import Q def role_list(request): all_roles = models.Role.objects.all() return render(request, 'rbac/role_list.html', {"all_roles": all_roles}) def role(request, edit_id=None): obj = models.Role.objects.filter(id=edit_id).first() form_obj = RoleForm(instance=obj) if request.method == 'POST': form_obj = RoleForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:role_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj}) def del_role(request, del_id): models.Role.objects.filter(id=del_id).delete() return redirect(reverse('rbac:role_list')) # 菜单信息 权限信息 def menu_list(request): all_menu = models.Menu.objects.all() mid = request.GET.get('mid') if mid: permission_query = models.Permission.objects.filter(Q(menu_id=mid) | Q(parent__menu_id=mid)) else: permission_query = models.Permission.objects.all() all_permission = permission_query.values('id', 'url', 'title', 'name', 'menu_id', 'parent_id','menu__title') all_permission_dict = {} for item in all_permission: menu_id = item.get('menu_id') if menu_id: item['children'] = [] all_permission_dict[item['id']] = item for item in all_permission: pid = item.get('parent_id') if pid: all_permission_dict[pid]['children'].append(item) print(all_permission_dict) return render(request, 'rbac/menu_list.html', {"all_menu": all_menu, 'all_permission_dict': all_permission_dict, 'mid': mid}) def menu(request, edit_id=None): obj = models.Menu.objects.filter(id=edit_id).first() form_obj = MenuForm(instance=obj) if request.method == 'POST': form_obj = MenuForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:menu_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj})
{% extends 'layout.html' %} {% block css %} <style> .permission-area tr.parent { background-color: #cae7fd;; } .menu-body tr.active { background-color: #f1f7fd; border-left: 3px solid #fdc00f; } </style> {% endblock %} {% block content %} <div style="margin: 20px"> <div class="col-sm-3"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><i class="fa fa-book"></i> 菜单管理 <a href="{% url 'rbac:menu_add' %}" class="btn btn-success btn-sm pull-right " style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-plus"></i> 新建</a> </div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>图标</th> <th>操作</th> </tr> </thead> <tbody class="menu-body"> {% for menu in all_menu %} <tr class=" {% if menu.id|safe == mid %} active {% endif %} "> <td><a href="?mid={{ menu.id }}">{{ menu.title }}</a></td> <td><i class="fa {{ menu.icon }} "></i></td> <td> <a href="{% url 'rbac:menu_edit' menu.id %}"> <i class="fa fa-edit"></i> </a> <a href=""> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> </div> <div class="col-sm-9"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><i class="fa fa-cubes"></i> 权限管理</div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>URL</th> <th>URL别名</th> <th>菜单</th> <th>所属菜单</th> <th>操作</th> </tr> </thead> <tbody class="permission-area"> {% for p_permission in all_permission_dict.values %} <tr class="parent" id="{{ p_permission.id }}"> <td class="title"> <i class="fa fa-caret-down"></i> {{ p_permission.title }} </td> <td>{{ p_permission.url }}</td> <td>{{ p_permission.name }}</td> <td> 是 </td> <td> {{ p_permission.menu__title }} </td> <td> <a href=""> <i class="fa fa-edit"></i> </a> <a href=""> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% for c_permission in p_permission.children %} <tr pid="{{ c_permission.parent_id }}"> <td>{{ c_permission.title }}</td> <td>{{ c_permission.url }}</td> <td>{{ c_permission.name }}</td> <td></td> <td></td> <td> <a href=""> <i class="fa fa-edit"></i> </a> <a href=""> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} {% endfor %} </tbody> </table> </div> </div> </div> {% endblock %} {% block js %} <script> $('.permission-area').on('click', '.parent .title', function () { var caret = $(this).find('i'); var id = $(this).parent().attr('id'); if (caret.hasClass('fa-caret-right')) { caret.removeClass('fa-caret-right').addClass('fa-caret-down'); $(this).parent().nextAll('tr[pid="' + id + '"]').removeClass('hide'); } else { caret.removeClass('fa-caret-down').addClass('fa-caret-right'); $(this).parent().nextAll('tr[pid="' + id + '"]').addClass('hide'); } }) </script> {% endblock %}
最终版:权限表增加,编辑,批量操作,权限分配
from django.db import models class Menu(models.Model): """ 一级菜单 """ title = models.CharField(max_length=32, unique=True,verbose_name='标题') icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True) weight = models.IntegerField(default=1,verbose_name='权重') class Meta: verbose_name_plural = '菜单表' verbose_name = '菜单表' def __str__(self): return self.title class Permission(models.Model): """ 权限表 有关联Menu的是二级菜单 没有关联Menu的不是二级菜单,是不可以做菜单的权限 """ title = models.CharField(max_length=32, verbose_name='标题') url = models.CharField(max_length=32, verbose_name='权限') menu = models.ForeignKey('Menu', null=True, blank=True,verbose_name='菜单') parent = models.ForeignKey('Permission', null=True, blank=True,verbose_name='父权限') name = models.CharField(max_length=32, null=True, blank=True, unique=True,verbose_name='URL别名') class Meta: verbose_name_plural = '权限表' verbose_name = '权限表' def __str__(self): return self.title class Role(models.Model): name = models.CharField(max_length=32, verbose_name='角色名称') permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True) def __str__(self): return self.name class User(models.Model): """ 用户表 """ name = models.CharField(max_length=32, verbose_name='用户名') password = models.CharField(max_length=32, verbose_name='密码') roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True) def __str__(self): return self.name
from django.conf.urls import url from rbac import views urlpatterns = [ # /app01/role/list/ # rbac:role_list url(r'^role/list/$', views.role_list, name='role_list'), url(r'^role/add/$', views.role, name='role_add'), url(r'^role/edit/(\d+)$', views.role, name='role_edit'), url(r'^role/del/(\d+)$', views.del_role, name='role_del'), url(r'^menu/list/$', views.menu_list, name='menu_list'), url(r'^menu/add/$', views.menu, name='menu_add'), url(r'^menu/edit/(\d+)$', views.menu, name='menu_edit'), url(r'^permission/add/$', views.permission, name='permission_add'), url(r'^permission/edit/(\d+)$', views.permission, name='permission_edit'), url(r'^permission/del/(\d+)$', views.del_permission, name='permission_del'), url(r'^multi/permissions/$', views.multi_permissions, name='multi_permissions'), url(r'^distribute/permissions/$', views.distribute_permissions, name='distribute_permissions'), ]
from django import forms from rbac import models from django.utils.safestring import mark_safe # 角色的Form class RoleForm(forms.ModelForm): class Meta: model = models.Role fields = ['name'] widgets = { 'name': forms.widgets.Input(attrs={"class": 'form-control'}) } ICON_LIST = [[i[0], mark_safe(i[1])] for i in [ ['fa-address-book', '<i aria-hidden="true" class="fa fa-address-book"></i>'], ['fa-address-book-o', '<i aria-hidden="true" class="fa fa-address-book-o"></i>'], ['fa-address-card', '<i aria-hidden="true" class="fa fa-address-card"></i>'], ['fa-address-card-o', '<i aria-hidden="true" class="fa fa-address-card-o"></i>'], ['fa-adjust', '<i aria-hidden="true" class="fa fa-adjust"></i>'], ['fa-american-sign-language-interpreting', '<i aria-hidden="true" class="fa fa-american-sign-language-interpreting"></i>'], ['fa-anchor', '<i aria-hidden="true" class="fa fa-anchor"></i>'], ['fa-archive', '<i aria-hidden="true" class="fa fa-archive"></i>'], ['fa-area-chart', '<i aria-hidden="true" class="fa fa-area-chart"></i>'], ['fa-arrows', '<i aria-hidden="true" class="fa fa-arrows"></i>'], ['fa-arrows-h', '<i aria-hidden="true" class="fa fa-arrows-h"></i>'], ['fa-arrows-v', '<i aria-hidden="true" class="fa fa-arrows-v"></i>'], ['fa-asl-interpreting', '<i aria-hidden="true" class="fa fa-asl-interpreting"></i>'], ['fa-assistive-listening-systems', '<i aria-hidden="true" class="fa fa-assistive-listening-systems"></i>'], ['fa-asterisk', '<i aria-hidden="true" class="fa fa-asterisk"></i>'], ['fa-at', '<i aria-hidden="true" class="fa fa-at"></i>'], ['fa-audio-description', '<i aria-hidden="true" class="fa fa-audio-description"></i>'], ['fa-automobile', '<i aria-hidden="true" class="fa fa-automobile"></i>'], ['fa-balance-scale', '<i aria-hidden="true" class="fa fa-balance-scale"></i>'], ['fa-ban', '<i aria-hidden="true" class="fa fa-ban"></i>'], ['fa-bank', '<i aria-hidden="true" class="fa fa-bank"></i>'], ['fa-bar-chart', '<i aria-hidden="true" class="fa fa-bar-chart"></i>'], ['fa-bar-chart-o', '<i aria-hidden="true" class="fa fa-bar-chart-o"></i>'], ['fa-barcode', '<i aria-hidden="true" class="fa fa-barcode"></i>'], ['fa-bars', '<i aria-hidden="true" class="fa fa-bars"></i>'], ['fa-bath', '<i aria-hidden="true" class="fa fa-bath"></i>'], ['fa-bathtub', '<i aria-hidden="true" class="fa fa-bathtub"></i>'], ['fa-battery', '<i aria-hidden="true" class="fa fa-battery"></i>'], ['fa-battery-0', '<i aria-hidden="true" class="fa fa-battery-0"></i>'], ['fa-battery-1', '<i aria-hidden="true" class="fa fa-battery-1"></i>'], ['fa-battery-2', '<i aria-hidden="true" class="fa fa-battery-2"></i>'], ['fa-battery-3', '<i aria-hidden="true" class="fa fa-battery-3"></i>'], ['fa-battery-4', '<i aria-hidden="true" class="fa fa-battery-4"></i>'], ['fa-battery-empty', '<i aria-hidden="true" class="fa fa-battery-empty"></i>'], ['fa-battery-full', '<i aria-hidden="true" class="fa fa-battery-full"></i>'], ['fa-battery-half', '<i aria-hidden="true" class="fa fa-battery-half"></i>'], ['fa-battery-quarter', '<i aria-hidden="true" class="fa fa-battery-quarter"></i>'], ['fa-battery-three-quarters', '<i aria-hidden="true" class="fa fa-battery-three-quarters"></i>'], ['fa-bed', '<i aria-hidden="true" class="fa fa-bed"></i>'], ['fa-beer', '<i aria-hidden="true" class="fa fa-beer"></i>'], ['fa-bell', '<i aria-hidden="true" class="fa fa-bell"></i>'], ['fa-bell-o', '<i aria-hidden="true" class="fa fa-bell-o"></i>'], ['fa-bell-slash', '<i aria-hidden="true" class="fa fa-bell-slash"></i>'], ['fa-bell-slash-o', '<i aria-hidden="true" class="fa fa-bell-slash-o"></i>'], ['fa-bicycle', '<i aria-hidden="true" class="fa fa-bicycle"></i>'], ['fa-binoculars', '<i aria-hidden="true" class="fa fa-binoculars"></i>'], ['fa-birthday-cake', '<i aria-hidden="true" class="fa fa-birthday-cake"></i>'], ['fa-blind', '<i aria-hidden="true" class="fa fa-blind"></i>'], ['fa-bluetooth', '<i aria-hidden="true" class="fa fa-bluetooth"></i>'], ['fa-bluetooth-b', '<i aria-hidden="true" class="fa fa-bluetooth-b"></i>'], ['fa-bolt', '<i aria-hidden="true" class="fa fa-bolt"></i>'], ['fa-bomb', '<i aria-hidden="true" class="fa fa-bomb"></i>'], ['fa-book', '<i aria-hidden="true" class="fa fa-book"></i>'], ['fa-bookmark', '<i aria-hidden="true" class="fa fa-bookmark"></i>'], ['fa-bookmark-o', '<i aria-hidden="true" class="fa fa-bookmark-o"></i>'], ['fa-braille', '<i aria-hidden="true" class="fa fa-braille"></i>'], ['fa-briefcase', '<i aria-hidden="true" class="fa fa-briefcase"></i>'], ['fa-bug', '<i aria-hidden="true" class="fa fa-bug"></i>'], ['fa-building', '<i aria-hidden="true" class="fa fa-building"></i>'], ['fa-building-o', '<i aria-hidden="true" class="fa fa-building-o"></i>'], ['fa-bullhorn', '<i aria-hidden="true" class="fa fa-bullhorn"></i>'], ['fa-bullseye', '<i aria-hidden="true" class="fa fa-bullseye"></i>'], ['fa-bus', '<i aria-hidden="true" class="fa fa-bus"></i>'], ['fa-cab', '<i aria-hidden="true" class="fa fa-cab"></i>'], ['fa-calculator', '<i aria-hidden="true" class="fa fa-calculator"></i>'], ['fa-calendar', '<i aria-hidden="true" class="fa fa-calendar"></i>'], ['fa-calendar-check-o', '<i aria-hidden="true" class="fa fa-calendar-check-o"></i>'], ['fa-calendar-minus-o', '<i aria-hidden="true" class="fa fa-calendar-minus-o"></i>'], ['fa-calendar-o', '<i aria-hidden="true" class="fa fa-calendar-o"></i>'], ['fa-calendar-plus-o', '<i aria-hidden="true" class="fa fa-calendar-plus-o"></i>'], ['fa-calendar-times-o', '<i aria-hidden="true" class="fa fa-calendar-times-o"></i>'], ['fa-camera', '<i aria-hidden="true" class="fa fa-camera"></i>'], ['fa-camera-retro', '<i aria-hidden="true" class="fa fa-camera-retro"></i>'], ['fa-car', '<i aria-hidden="true" class="fa fa-car"></i>'], ['fa-caret-square-o-down', '<i aria-hidden="true" class="fa fa-caret-square-o-down"></i>'], ['fa-caret-square-o-left', '<i aria-hidden="true" class="fa fa-caret-square-o-left"></i>'], ['fa-caret-square-o-right', '<i aria-hidden="true" class="fa fa-caret-square-o-right"></i>'], ['fa-caret-square-o-up', '<i aria-hidden="true" class="fa fa-caret-square-o-up"></i>'], ['fa-cart-arrow-down', '<i aria-hidden="true" class="fa fa-cart-arrow-down"></i>'], ['fa-cart-plus', '<i aria-hidden="true" class="fa fa-cart-plus"></i>'], ['fa-cc', '<i aria-hidden="true" class="fa fa-cc"></i>'], ['fa-certificate', '<i aria-hidden="true" class="fa fa-certificate"></i>'], ['fa-check', '<i aria-hidden="true" class="fa fa-check"></i>'], ['fa-check-circle', '<i aria-hidden="true" class="fa fa-check-circle"></i>'], ['fa-check-circle-o', '<i aria-hidden="true" class="fa fa-check-circle-o"></i>'], ['fa-check-square', '<i aria-hidden="true" class="fa fa-check-square"></i>'], ['fa-check-square-o', '<i aria-hidden="true" class="fa fa-check-square-o"></i>'], ['fa-child', '<i aria-hidden="true" class="fa fa-child"></i>'], ['fa-circle', '<i aria-hidden="true" class="fa fa-circle"></i>'], ['fa-circle-o', '<i aria-hidden="true" class="fa fa-circle-o"></i>'], ['fa-circle-o-notch', '<i aria-hidden="true" class="fa fa-circle-o-notch"></i>'], ['fa-circle-thin', '<i aria-hidden="true" class="fa fa-circle-thin"></i>'], ['fa-clock-o', '<i aria-hidden="true" class="fa fa-clock-o"></i>'], ['fa-clone', '<i aria-hidden="true" class="fa fa-clone"></i>'], ['fa-close', '<i aria-hidden="true" class="fa fa-close"></i>'], ['fa-cloud', '<i aria-hidden="true" class="fa fa-cloud"></i>'], ['fa-cloud-download', '<i aria-hidden="true" class="fa fa-cloud-download"></i>'], ['fa-cloud-upload', '<i aria-hidden="true" class="fa fa-cloud-upload"></i>'], ['fa-code', '<i aria-hidden="true" class="fa fa-code"></i>'], ['fa-code-fork', '<i aria-hidden="true" class="fa fa-code-fork"></i>'], ['fa-coffee', '<i aria-hidden="true" class="fa fa-coffee"></i>'], ['fa-cog', '<i aria-hidden="true" class="fa fa-cog"></i>'], ['fa-cogs', '<i aria-hidden="true" class="fa fa-cogs"></i>'], ['fa-comment', '<i aria-hidden="true" class="fa fa-comment"></i>'], ['fa-comment-o', '<i aria-hidden="true" class="fa fa-comment-o"></i>'], ['fa-commenting', '<i aria-hidden="true" class="fa fa-commenting"></i>'], ['fa-commenting-o', '<i aria-hidden="true" class="fa fa-commenting-o"></i>'], ['fa-comments', '<i aria-hidden="true" class="fa fa-comments"></i>'], ['fa-comments-o', '<i aria-hidden="true" class="fa fa-comments-o"></i>'], ['fa-compass', '<i aria-hidden="true" class="fa fa-compass"></i>'], ['fa-copyright', '<i aria-hidden="true" class="fa fa-copyright"></i>'], ['fa-creative-commons', '<i aria-hidden="true" class="fa fa-creative-commons"></i>'], ['fa-credit-card', '<i aria-hidden="true" class="fa fa-credit-card"></i>'], ['fa-credit-card-alt', '<i aria-hidden="true" class="fa fa-credit-card-alt"></i>'], ['fa-crop', '<i aria-hidden="true" class="fa fa-crop"></i>'], ['fa-crosshairs', '<i aria-hidden="true" class="fa fa-crosshairs"></i>'], ['fa-cube', '<i aria-hidden="true" class="fa fa-cube"></i>'], ['fa-cubes', '<i aria-hidden="true" class="fa fa-cubes"></i>'], ['fa-cutlery', '<i aria-hidden="true" class="fa fa-cutlery"></i>'], ['fa-dashboard', '<i aria-hidden="true" class="fa fa-dashboard"></i>'], ['fa-database', '<i aria-hidden="true" class="fa fa-database"></i>'], ['fa-deaf', '<i aria-hidden="true" class="fa fa-deaf"></i>'], ['fa-deafness', '<i aria-hidden="true" class="fa fa-deafness"></i>'], ['fa-desktop', '<i aria-hidden="true" class="fa fa-desktop"></i>'], ['fa-diamond', '<i aria-hidden="true" class="fa fa-diamond"></i>'], ['fa-dot-circle-o', '<i aria-hidden="true" class="fa fa-dot-circle-o"></i>'], ['fa-download', '<i aria-hidden="true" class="fa fa-download"></i>'], ['fa-drivers-license', '<i aria-hidden="true" class="fa fa-drivers-license"></i>'], ['fa-drivers-license-o', '<i aria-hidden="true" class="fa fa-drivers-license-o"></i>'], ['fa-edit', '<i aria-hidden="true" class="fa fa-edit"></i>'], ['fa-ellipsis-h', '<i aria-hidden="true" class="fa fa-ellipsis-h"></i>'], ['fa-ellipsis-v', '<i aria-hidden="true" class="fa fa-ellipsis-v"></i>'], ['fa-envelope', '<i aria-hidden="true" class="fa fa-envelope"></i>'], ['fa-envelope-o', '<i aria-hidden="true" class="fa fa-envelope-o"></i>'], ['fa-envelope-open', '<i aria-hidden="true" class="fa fa-envelope-open"></i>'], ['fa-envelope-open-o', '<i aria-hidden="true" class="fa fa-envelope-open-o"></i>'], ['fa-envelope-square', '<i aria-hidden="true" class="fa fa-envelope-square"></i>'], ['fa-eraser', '<i aria-hidden="true" class="fa fa-eraser"></i>'], ['fa-exchange', '<i aria-hidden="true" class="fa fa-exchange"></i>'], ['fa-exclamation', '<i aria-hidden="true" class="fa fa-exclamation"></i>'], ['fa-exclamation-circle', '<i aria-hidden="true" class="fa fa-exclamation-circle"></i>'], ['fa-exclamation-triangle', '<i aria-hidden="true" class="fa fa-exclamation-triangle"></i>'], ['fa-external-link', '<i aria-hidden="true" class="fa fa-external-link"></i>'], ['fa-external-link-square', '<i aria-hidden="true" class="fa fa-external-link-square"></i>'], ['fa-eye', '<i aria-hidden="true" class="fa fa-eye"></i>'], ['fa-eye-slash', '<i aria-hidden="true" class="fa fa-eye-slash"></i>'], ['fa-eyedropper', '<i aria-hidden="true" class="fa fa-eyedropper"></i>'], ['fa-fax', '<i aria-hidden="true" class="fa fa-fax"></i>'], ['fa-feed', '<i aria-hidden="true" class="fa fa-feed"></i>'], ['fa-female', '<i aria-hidden="true" class="fa fa-female"></i>'], ['fa-fighter-jet', '<i aria-hidden="true" class="fa fa-fighter-jet"></i>'], ['fa-file-archive-o', '<i aria-hidden="true" class="fa fa-file-archive-o"></i>'], ['fa-file-audio-o', '<i aria-hidden="true" class="fa fa-file-audio-o"></i>'], ['fa-file-code-o', '<i aria-hidden="true" class="fa fa-file-code-o"></i>'], ['fa-file-excel-o', '<i aria-hidden="true" class="fa fa-file-excel-o"></i>'], ['fa-file-image-o', '<i aria-hidden="true" class="fa fa-file-image-o"></i>'], ['fa-file-movie-o', '<i aria-hidden="true" class="fa fa-file-movie-o"></i>'], ['fa-file-pdf-o', '<i aria-hidden="true" class="fa fa-file-pdf-o"></i>'], ['fa-file-photo-o', '<i aria-hidden="true" class="fa fa-file-photo-o"></i>'], ['fa-file-picture-o', '<i aria-hidden="true" class="fa fa-file-picture-o"></i>'], ['fa-file-powerpoint-o', '<i aria-hidden="true" class="fa fa-file-powerpoint-o"></i>'], ['fa-file-sound-o', '<i aria-hidden="true" class="fa fa-file-sound-o"></i>'], ['fa-file-video-o', '<i aria-hidden="true" class="fa fa-file-video-o"></i>'], ['fa-file-word-o', '<i aria-hidden="true" class="fa fa-file-word-o"></i>'], ['fa-file-zip-o', '<i aria-hidden="true" class="fa fa-file-zip-o"></i>'], ['fa-film', '<i aria-hidden="true" class="fa fa-film"></i>'], ['fa-filter', '<i aria-hidden="true" class="fa fa-filter"></i>'], ['fa-fire', '<i aria-hidden="true" class="fa fa-fire"></i>'], ['fa-fire-extinguisher', '<i aria-hidden="true" class="fa fa-fire-extinguisher"></i>'], ['fa-flag', '<i aria-hidden="true" class="fa fa-flag"></i>'], ['fa-flag-checkered', '<i aria-hidden="true" class="fa fa-flag-checkered"></i>'], ['fa-flag-o', '<i aria-hidden="true" class="fa fa-flag-o"></i>'], ['fa-flash', '<i aria-hidden="true" class="fa fa-flash"></i>'], ['fa-flask', '<i aria-hidden="true" class="fa fa-flask"></i>'], ['fa-folder', '<i aria-hidden="true" class="fa fa-folder"></i>'], ['fa-folder-o', '<i aria-hidden="true" class="fa fa-folder-o"></i>'], ['fa-folder-open', '<i aria-hidden="true" class="fa fa-folder-open"></i>'], ['fa-folder-open-o', '<i aria-hidden="true" class="fa fa-folder-open-o"></i>'], ['fa-frown-o', '<i aria-hidden="true" class="fa fa-frown-o"></i>'], ['fa-futbol-o', '<i aria-hidden="true" class="fa fa-futbol-o"></i>'], ['fa-gamepad', '<i aria-hidden="true" class="fa fa-gamepad"></i>'], ['fa-gavel', '<i aria-hidden="true" class="fa fa-gavel"></i>'], ['fa-gear', '<i aria-hidden="true" class="fa fa-gear"></i>'], ['fa-gears', '<i aria-hidden="true" class="fa fa-gears"></i>'], ['fa-gift', '<i aria-hidden="true" class="fa fa-gift"></i>'], ['fa-glass', '<i aria-hidden="true" class="fa fa-glass"></i>'], ['fa-globe', '<i aria-hidden="true" class="fa fa-globe"></i>'], ['fa-graduation-cap', '<i aria-hidden="true" class="fa fa-graduation-cap"></i>'], ['fa-group', '<i aria-hidden="true" class="fa fa-group"></i>'], ['fa-hand-grab-o', '<i aria-hidden="true" class="fa fa-hand-grab-o"></i>'], ['fa-hand-lizard-o', '<i aria-hidden="true" class="fa fa-hand-lizard-o"></i>'], ['fa-hand-paper-o', '<i aria-hidden="true" class="fa fa-hand-paper-o"></i>'], ['fa-hand-peace-o', '<i aria-hidden="true" class="fa fa-hand-peace-o"></i>'], ['fa-hand-pointer-o', '<i aria-hidden="true" class="fa fa-hand-pointer-o"></i>'], ['fa-hand-rock-o', '<i aria-hidden="true" class="fa fa-hand-rock-o"></i>'], ['fa-hand-scissors-o', '<i aria-hidden="true" class="fa fa-hand-scissors-o"></i>'], ['fa-hand-spock-o', '<i aria-hidden="true" class="fa fa-hand-spock-o"></i>'], ['fa-hand-stop-o', '<i aria-hidden="true" class="fa fa-hand-stop-o"></i>'], ['fa-handshake-o', '<i aria-hidden="true" class="fa fa-handshake-o"></i>'], ['fa-hard-of-hearing', '<i aria-hidden="true" class="fa fa-hard-of-hearing"></i>'], ['fa-hashtag', '<i aria-hidden="true" class="fa fa-hashtag"></i>'], ['fa-hdd-o', '<i aria-hidden="true" class="fa fa-hdd-o"></i>'], ['fa-headphones', '<i aria-hidden="true" class="fa fa-headphones"></i>'], ['fa-heart', '<i aria-hidden="true" class="fa fa-heart"></i>'], ['fa-heart-o', '<i aria-hidden="true" class="fa fa-heart-o"></i>'], ['fa-heartbeat', '<i aria-hidden="true" class="fa fa-heartbeat"></i>'], ['fa-history', '<i aria-hidden="true" class="fa fa-history"></i>'], ['fa-home', '<i aria-hidden="true" class="fa fa-home"></i>'], ['fa-hotel', '<i aria-hidden="true" class="fa fa-hotel"></i>'], ['fa-hourglass', '<i aria-hidden="true" class="fa fa-hourglass"></i>'], ['fa-hourglass-1', '<i aria-hidden="true" class="fa fa-hourglass-1"></i>'], ['fa-hourglass-2', '<i aria-hidden="true" class="fa fa-hourglass-2"></i>'], ['fa-hourglass-3', '<i aria-hidden="true" class="fa fa-hourglass-3"></i>'], ['fa-hourglass-end', '<i aria-hidden="true" class="fa fa-hourglass-end"></i>'], ['fa-hourglass-half', '<i aria-hidden="true" class="fa fa-hourglass-half"></i>'], ['fa-hourglass-o', '<i aria-hidden="true" class="fa fa-hourglass-o"></i>'], ['fa-hourglass-start', '<i aria-hidden="true" class="fa fa-hourglass-start"></i>'], ['fa-i-cursor', '<i aria-hidden="true" class="fa fa-i-cursor"></i>'], ['fa-id-badge', '<i aria-hidden="true" class="fa fa-id-badge"></i>'], ['fa-id-card', '<i aria-hidden="true" class="fa fa-id-card"></i>'], ['fa-id-card-o', '<i aria-hidden="true" class="fa fa-id-card-o"></i>'], ['fa-image', '<i aria-hidden="true" class="fa fa-image"></i>'], ['fa-inbox', '<i aria-hidden="true" class="fa fa-inbox"></i>'], ['fa-industry', '<i aria-hidden="true" class="fa fa-industry"></i>'], ['fa-info', '<i aria-hidden="true" class="fa fa-info"></i>'], ['fa-info-circle', '<i aria-hidden="true" class="fa fa-info-circle"></i>'], ['fa-institution', '<i aria-hidden="true" class="fa fa-institution"></i>'], ['fa-key', '<i aria-hidden="true" class="fa fa-key"></i>'], ['fa-keyboard-o', '<i aria-hidden="true" class="fa fa-keyboard-o"></i>'], ['fa-language', '<i aria-hidden="true" class="fa fa-language"></i>'], ['fa-laptop', '<i aria-hidden="true" class="fa fa-laptop"></i>'], ['fa-leaf', '<i aria-hidden="true" class="fa fa-leaf"></i>'], ['fa-legal', '<i aria-hidden="true" class="fa fa-legal"></i>'], ['fa-lemon-o', '<i aria-hidden="true" class="fa fa-lemon-o"></i>'], ['fa-level-down', '<i aria-hidden="true" class="fa fa-level-down"></i>'], ['fa-level-up', '<i aria-hidden="true" class="fa fa-level-up"></i>'], ['fa-life-bouy', '<i aria-hidden="true" class="fa fa-life-bouy"></i>'], ['fa-life-buoy', '<i aria-hidden="true" class="fa fa-life-buoy"></i>'], ['fa-life-ring', '<i aria-hidden="true" class="fa fa-life-ring"></i>'], ['fa-life-saver', '<i aria-hidden="true" class="fa fa-life-saver"></i>'], ['fa-lightbulb-o', '<i aria-hidden="true" class="fa fa-lightbulb-o"></i>'], ['fa-line-chart', '<i aria-hidden="true" class="fa fa-line-chart"></i>'], ['fa-location-arrow', '<i aria-hidden="true" class="fa fa-location-arrow"></i>'], ['fa-lock', '<i aria-hidden="true" class="fa fa-lock"></i>'], ['fa-low-vision', '<i aria-hidden="true" class="fa fa-low-vision"></i>'], ['fa-magic', '<i aria-hidden="true" class="fa fa-magic"></i>'], ['fa-magnet', '<i aria-hidden="true" class="fa fa-magnet"></i>'], ['fa-mail-forward', '<i aria-hidden="true" class="fa fa-mail-forward"></i>'], ['fa-mail-reply', '<i aria-hidden="true" class="fa fa-mail-reply"></i>'], ['fa-mail-reply-all', '<i aria-hidden="true" class="fa fa-mail-reply-all"></i>'], ['fa-male', '<i aria-hidden="true" class="fa fa-male"></i>'], ['fa-map', '<i aria-hidden="true" class="fa fa-map"></i>'], ['fa-map-marker', '<i aria-hidden="true" class="fa fa-map-marker"></i>'], ['fa-map-o', '<i aria-hidden="true" class="fa fa-map-o"></i>'], ['fa-map-pin', '<i aria-hidden="true" class="fa fa-map-pin"></i>'], ['fa-map-signs', '<i aria-hidden="true" class="fa fa-map-signs"></i>'], ['fa-meh-o', '<i aria-hidden="true" class="fa fa-meh-o"></i>'], ['fa-microchip', '<i aria-hidden="true" class="fa fa-microchip"></i>'], ['fa-microphone', '<i aria-hidden="true" class="fa fa-microphone"></i>'], ['fa-microphone-slash', '<i aria-hidden="true" class="fa fa-microphone-slash"></i>'], ['fa-minus', '<i aria-hidden="true" class="fa fa-minus"></i>'], ['fa-minus-circle', '<i aria-hidden="true" class="fa fa-minus-circle"></i>'], ['fa-minus-square', '<i aria-hidden="true" class="fa fa-minus-square"></i>'], ['fa-minus-square-o', '<i aria-hidden="true" class="fa fa-minus-square-o"></i>'], ['fa-mobile', '<i aria-hidden="true" class="fa fa-mobile"></i>'], ['fa-mobile-phone', '<i aria-hidden="true" class="fa fa-mobile-phone"></i>'], ['fa-money', '<i aria-hidden="true" class="fa fa-money"></i>'], ['fa-moon-o', '<i aria-hidden="true" class="fa fa-moon-o"></i>'], ['fa-mortar-board', '<i aria-hidden="true" class="fa fa-mortar-board"></i>'], ['fa-motorcycle', '<i aria-hidden="true" class="fa fa-motorcycle"></i>'], ['fa-mouse-pointer', '<i aria-hidden="true" class="fa fa-mouse-pointer"></i>'], ['fa-music', '<i aria-hidden="true" class="fa fa-music"></i>'], ['fa-navicon', '<i aria-hidden="true" class="fa fa-navicon"></i>'], ['fa-newspaper-o', '<i aria-hidden="true" class="fa fa-newspaper-o"></i>'], ['fa-object-group', '<i aria-hidden="true" class="fa fa-object-group"></i>'], ['fa-object-ungroup', '<i aria-hidden="true" class="fa fa-object-ungroup"></i>'], ['fa-paint-brush', '<i aria-hidden="true" class="fa fa-paint-brush"></i>'], ['fa-paper-plane', '<i aria-hidden="true" class="fa fa-paper-plane"></i>'], ['fa-paper-plane-o', '<i aria-hidden="true" class="fa fa-paper-plane-o"></i>'], ['fa-paw', '<i aria-hidden="true" class="fa fa-paw"></i>'], ['fa-pencil', '<i aria-hidden="true" class="fa fa-pencil"></i>'], ['fa-pencil-square', '<i aria-hidden="true" class="fa fa-pencil-square"></i>'], ['fa-pencil-square-o', '<i aria-hidden="true" class="fa fa-pencil-square-o"></i>'], ['fa-percent', '<i aria-hidden="true" class="fa fa-percent"></i>'], ['fa-phone', '<i aria-hidden="true" class="fa fa-phone"></i>'], ['fa-phone-square', '<i aria-hidden="true" class="fa fa-phone-square"></i>'], ['fa-photo', '<i aria-hidden="true" class="fa fa-photo"></i>'], ['fa-picture-o', '<i aria-hidden="true" class="fa fa-picture-o"></i>'], ['fa-pie-chart', '<i aria-hidden="true" class="fa fa-pie-chart"></i>'], ['fa-plane', '<i aria-hidden="true" class="fa fa-plane"></i>'], ['fa-plug', '<i aria-hidden="true" class="fa fa-plug"></i>'], ['fa-plus', '<i aria-hidden="true" class="fa fa-plus"></i>'], ['fa-plus-circle', '<i aria-hidden="true" class="fa fa-plus-circle"></i>'], ['fa-plus-square', '<i aria-hidden="true" class="fa fa-plus-square"></i>'], ['fa-plus-square-o', '<i aria-hidden="true" class="fa fa-plus-square-o"></i>'], ['fa-podcast', '<i aria-hidden="true" class="fa fa-podcast"></i>'], ['fa-power-off', '<i aria-hidden="true" class="fa fa-power-off"></i>'], ['fa-print', '<i aria-hidden="true" class="fa fa-print"></i>'], ['fa-puzzle-piece', '<i aria-hidden="true" class="fa fa-puzzle-piece"></i>'], ['fa-qrcode', '<i aria-hidden="true" class="fa fa-qrcode"></i>'], ['fa-question', '<i aria-hidden="true" class="fa fa-question"></i>'], ['fa-question-circle', '<i aria-hidden="true" class="fa fa-question-circle"></i>'], ['fa-question-circle-o', '<i aria-hidden="true" class="fa fa-question-circle-o"></i>'], ['fa-quote-left', '<i aria-hidden="true" class="fa fa-quote-left"></i>'], ['fa-quote-right', '<i aria-hidden="true" class="fa fa-quote-right"></i>'], ['fa-random', '<i aria-hidden="true" class="fa fa-random"></i>'], ['fa-recycle', '<i aria-hidden="true" class="fa fa-recycle"></i>'], ['fa-refresh', '<i aria-hidden="true" class="fa fa-refresh"></i>'], ['fa-registered', '<i aria-hidden="true" class="fa fa-registered"></i>'], ['fa-remove', '<i aria-hidden="true" class="fa fa-remove"></i>'], ['fa-reorder', '<i aria-hidden="true" class="fa fa-reorder"></i>'], ['fa-reply', '<i aria-hidden="true" class="fa fa-reply"></i>'], ['fa-reply-all', '<i aria-hidden="true" class="fa fa-reply-all"></i>'], ['fa-retweet', '<i aria-hidden="true" class="fa fa-retweet"></i>'], ['fa-road', '<i aria-hidden="true" class="fa fa-road"></i>'], ['fa-rocket', '<i aria-hidden="true" class="fa fa-rocket"></i>'], ['fa-rss', '<i aria-hidden="true" class="fa fa-rss"></i>'], ['fa-rss-square', '<i aria-hidden="true" class="fa fa-rss-square"></i>'], ['fa-s15', '<i aria-hidden="true" class="fa fa-s15"></i>'], ['fa-search', '<i aria-hidden="true" class="fa fa-search"></i>'], ['fa-search-minus', '<i aria-hidden="true" class="fa fa-search-minus"></i>'], ['fa-search-plus', '<i aria-hidden="true" class="fa fa-search-plus"></i>'], ['fa-send', '<i aria-hidden="true" class="fa fa-send"></i>'], ['fa-send-o', '<i aria-hidden="true" class="fa fa-send-o"></i>'], ['fa-server', '<i aria-hidden="true" class="fa fa-server"></i>'], ['fa-share', '<i aria-hidden="true" class="fa fa-share"></i>'], ['fa-share-alt', '<i aria-hidden="true" class="fa fa-share-alt"></i>'], ['fa-share-alt-square', '<i aria-hidden="true" class="fa fa-share-alt-square"></i>'], ['fa-share-square', '<i aria-hidden="true" class="fa fa-share-square"></i>'], ['fa-share-square-o', '<i aria-hidden="true" class="fa fa-share-square-o"></i>'], ['fa-shield', '<i aria-hidden="true" class="fa fa-shield"></i>'], ['fa-ship', '<i aria-hidden="true" class="fa fa-ship"></i>'], ['fa-shopping-bag', '<i aria-hidden="true" class="fa fa-shopping-bag"></i>'], ['fa-shopping-basket', '<i aria-hidden="true" class="fa fa-shopping-basket"></i>'], ['fa-shopping-cart', '<i aria-hidden="true" class="fa fa-shopping-cart"></i>'], ['fa-shower', '<i aria-hidden="true" class="fa fa-shower"></i>'], ['fa-sign-in', '<i aria-hidden="true" class="fa fa-sign-in"></i>'], ['fa-sign-language', '<i aria-hidden="true" class="fa fa-sign-language"></i>'], ['fa-sign-out', '<i aria-hidden="true" class="fa fa-sign-out"></i>'], ['fa-signal', '<i aria-hidden="true" class="fa fa-signal"></i>'], ['fa-signing', '<i aria-hidden="true" class="fa fa-signing"></i>'], ['fa-sitemap', '<i aria-hidden="true" class="fa fa-sitemap"></i>'], ['fa-sliders', '<i aria-hidden="true" class="fa fa-sliders"></i>'], ['fa-smile-o', '<i aria-hidden="true" class="fa fa-smile-o"></i>'], ['fa-snowflake-o', '<i aria-hidden="true" class="fa fa-snowflake-o"></i>'], ['fa-soccer-ball-o', '<i aria-hidden="true" class="fa fa-soccer-ball-o"></i>'], ['fa-sort', '<i aria-hidden="true" class="fa fa-sort"></i>'], ['fa-sort-alpha-asc', '<i aria-hidden="true" class="fa fa-sort-alpha-asc"></i>'], ['fa-sort-alpha-desc', '<i aria-hidden="true" class="fa fa-sort-alpha-desc"></i>'], ['fa-sort-amount-asc', '<i aria-hidden="true" class="fa fa-sort-amount-asc"></i>'], ['fa-sort-amount-desc', '<i aria-hidden="true" class="fa fa-sort-amount-desc"></i>'], ['fa-sort-asc', '<i aria-hidden="true" class="fa fa-sort-asc"></i>'], ['fa-sort-desc', '<i aria-hidden="true" class="fa fa-sort-desc"></i>'], ['fa-sort-down', '<i aria-hidden="true" class="fa fa-sort-down"></i>'], ['fa-sort-numeric-asc', '<i aria-hidden="true" class="fa fa-sort-numeric-asc"></i>'], ['fa-sort-numeric-desc', '<i aria-hidden="true" class="fa fa-sort-numeric-desc"></i>'], ['fa-sort-up', '<i aria-hidden="true" class="fa fa-sort-up"></i>'], ['fa-space-shuttle', '<i aria-hidden="true" class="fa fa-space-shuttle"></i>'], ['fa-spinner', '<i aria-hidden="true" class="fa fa-spinner"></i>'], ['fa-spoon', '<i aria-hidden="true" class="fa fa-spoon"></i>'], ['fa-square', '<i aria-hidden="true" class="fa fa-square"></i>'], ['fa-square-o', '<i aria-hidden="true" class="fa fa-square-o"></i>'], ['fa-star', '<i aria-hidden="true" class="fa fa-star"></i>'], ['fa-star-half', '<i aria-hidden="true" class="fa fa-star-half"></i>'], ['fa-star-half-empty', '<i aria-hidden="true" class="fa fa-star-half-empty"></i>'], ['fa-star-half-full', '<i aria-hidden="true" class="fa fa-star-half-full"></i>'], ['fa-star-half-o', '<i aria-hidden="true" class="fa fa-star-half-o"></i>'], ['fa-star-o', '<i aria-hidden="true" class="fa fa-star-o"></i>'], ['fa-sticky-note', '<i aria-hidden="true" class="fa fa-sticky-note"></i>'], ['fa-sticky-note-o', '<i aria-hidden="true" class="fa fa-sticky-note-o"></i>'], ['fa-street-view', '<i aria-hidden="true" class="fa fa-street-view"></i>'], ['fa-suitcase', '<i aria-hidden="true" class="fa fa-suitcase"></i>'], ['fa-sun-o', '<i aria-hidden="true" class="fa fa-sun-o"></i>'], ['fa-support', '<i aria-hidden="true" class="fa fa-support"></i>'], ['fa-tablet', '<i aria-hidden="true" class="fa fa-tablet"></i>'], ['fa-tachometer', '<i aria-hidden="true" class="fa fa-tachometer"></i>'], ['fa-tag', '<i aria-hidden="true" class="fa fa-tag"></i>'], ['fa-tags', '<i aria-hidden="true" class="fa fa-tags"></i>'], ['fa-tasks', '<i aria-hidden="true" class="fa fa-tasks"></i>'], ['fa-taxi', '<i aria-hidden="true" class="fa fa-taxi"></i>'], ['fa-television', '<i aria-hidden="true" class="fa fa-television"></i>'], ['fa-terminal', '<i aria-hidden="true" class="fa fa-terminal"></i>'], ['fa-thermometer', '<i aria-hidden="true" class="fa fa-thermometer"></i>'], ['fa-thermometer-0', '<i aria-hidden="true" class="fa fa-thermometer-0"></i>'], ['fa-thermometer-1', '<i aria-hidden="true" class="fa fa-thermometer-1"></i>'], ['fa-thermometer-2', '<i aria-hidden="true" class="fa fa-thermometer-2"></i>'], ['fa-thermometer-3', '<i aria-hidden="true" class="fa fa-thermometer-3"></i>'], ['fa-thermometer-4', '<i aria-hidden="true" class="fa fa-thermometer-4"></i>'], ['fa-thermometer-empty', '<i aria-hidden="true" class="fa fa-thermometer-empty"></i>'], ['fa-thermometer-full', '<i aria-hidden="true" class="fa fa-thermometer-full"></i>'], ['fa-thermometer-half', '<i aria-hidden="true" class="fa fa-thermometer-half"></i>'], ['fa-thermometer-quarter', '<i aria-hidden="true" class="fa fa-thermometer-quarter"></i>'], ['fa-thermometer-three-quarters', '<i aria-hidden="true" class="fa fa-thermometer-three-quarters"></i>'], ['fa-thumb-tack', '<i aria-hidden="true" class="fa fa-thumb-tack"></i>'], ['fa-thumbs-down', '<i aria-hidden="true" class="fa fa-thumbs-down"></i>'], ['fa-thumbs-o-down', '<i aria-hidden="true" class="fa fa-thumbs-o-down"></i>'], ['fa-thumbs-o-up', '<i aria-hidden="true" class="fa fa-thumbs-o-up"></i>'], ['fa-thumbs-up', '<i aria-hidden="true" class="fa fa-thumbs-up"></i>'], ['fa-ticket', '<i aria-hidden="true" class="fa fa-ticket"></i>'], ['fa-times', '<i aria-hidden="true" class="fa fa-times"></i>'], ['fa-times-circle', '<i aria-hidden="true" class="fa fa-times-circle"></i>'], ['fa-times-circle-o', '<i aria-hidden="true" class="fa fa-times-circle-o"></i>'], ['fa-times-rectangle', '<i aria-hidden="true" class="fa fa-times-rectangle"></i>'], ['fa-times-rectangle-o', '<i aria-hidden="true" class="fa fa-times-rectangle-o"></i>'], ['fa-tint', '<i aria-hidden="true" class="fa fa-tint"></i>'], ['fa-toggle-down', '<i aria-hidden="true" class="fa fa-toggle-down"></i>'], ['fa-toggle-left', '<i aria-hidden="true" class="fa fa-toggle-left"></i>'], ['fa-toggle-off', '<i aria-hidden="true" class="fa fa-toggle-off"></i>'], ['fa-toggle-on', '<i aria-hidden="true" class="fa fa-toggle-on"></i>'], ['fa-toggle-right', '<i aria-hidden="true" class="fa fa-toggle-right"></i>'], ['fa-toggle-up', '<i aria-hidden="true" class="fa fa-toggle-up"></i>'], ['fa-trademark', '<i aria-hidden="true" class="fa fa-trademark"></i>'], ['fa-trash', '<i aria-hidden="true" class="fa fa-trash"></i>'], ['fa-trash-o', '<i aria-hidden="true" class="fa fa-trash-o"></i>'], ['fa-tree', '<i aria-hidden="true" class="fa fa-tree"></i>'], ['fa-trophy', '<i aria-hidden="true" class="fa fa-trophy"></i>'], ['fa-truck', '<i aria-hidden="true" class="fa fa-truck"></i>'], ['fa-tty', '<i aria-hidden="true" class="fa fa-tty"></i>'], ['fa-tv', '<i aria-hidden="true" class="fa fa-tv"></i>'], ['fa-umbrella', '<i aria-hidden="true" class="fa fa-umbrella"></i>'], ['fa-universal-access', '<i aria-hidden="true" class="fa fa-universal-access"></i>'], ['fa-university', '<i aria-hidden="true" class="fa fa-university"></i>'], ['fa-unlock', '<i aria-hidden="true" class="fa fa-unlock"></i>'], ['fa-unlock-alt', '<i aria-hidden="true" class="fa fa-unlock-alt"></i>'], ['fa-unsorted', '<i aria-hidden="true" class="fa fa-unsorted"></i>'], ['fa-upload', '<i aria-hidden="true" class="fa fa-upload"></i>'], ['fa-user', '<i aria-hidden="true" class="fa fa-user"></i>'], ['fa-user-circle', '<i aria-hidden="true" class="fa fa-user-circle"></i>'], ['fa-user-circle-o', '<i aria-hidden="true" class="fa fa-user-circle-o"></i>'], ['fa-user-o', '<i aria-hidden="true" class="fa fa-user-o"></i>'], ['fa-user-plus', '<i aria-hidden="true" class="fa fa-user-plus"></i>'], ['fa-user-secret', '<i aria-hidden="true" class="fa fa-user-secret"></i>'], ['fa-user-times', '<i aria-hidden="true" class="fa fa-user-times"></i>'], ['fa-users', '<i aria-hidden="true" class="fa fa-users"></i>'], ['fa-vcard', '<i aria-hidden="true" class="fa fa-vcard"></i>'], ['fa-vcard-o', '<i aria-hidden="true" class="fa fa-vcard-o"></i>'], ['fa-video-camera', '<i aria-hidden="true" class="fa fa-video-camera"></i>'], ['fa-volume-control-phone', '<i aria-hidden="true" class="fa fa-volume-control-phone"></i>'], ['fa-volume-down', '<i aria-hidden="true" class="fa fa-volume-down"></i>'], ['fa-volume-off', '<i aria-hidden="true" class="fa fa-volume-off"></i>'], ['fa-volume-up', '<i aria-hidden="true" class="fa fa-volume-up"></i>'], ['fa-warning', '<i aria-hidden="true" class="fa fa-warning"></i>'], ['fa-wheelchair', '<i aria-hidden="true" class="fa fa-wheelchair"></i>'], ['fa-wheelchair-alt', '<i aria-hidden="true" class="fa fa-wheelchair-alt"></i>'], ['fa-wifi', '<i aria-hidden="true" class="fa fa-wifi"></i>'], ['fa-window-close', '<i aria-hidden="true" class="fa fa-window-close"></i>'], ['fa-window-close-o', '<i aria-hidden="true" class="fa fa-window-close-o"></i>'], ['fa-window-maximize', '<i aria-hidden="true" class="fa fa-window-maximize"></i>'], ['fa-window-minimize', '<i aria-hidden="true" class="fa fa-window-minimize"></i>'], ['fa-window-restore', '<i aria-hidden="true" class="fa fa-window-restore"></i>'], ['fa-wrench', '<i aria-hidden="true" class="fa fa-wrench"></i>'] ]] # 菜单的Form class MenuForm(forms.ModelForm): class Meta: model = models.Menu fields = ['title', 'weight', 'icon', ] widgets = { 'title': forms.widgets.Input(attrs={"class": 'form-control'}), 'weight': forms.widgets.Input(attrs={"class": 'form-control'}), 'icon': forms.widgets.RadioSelect(choices=ICON_LIST), } class PermissionForm(forms.ModelForm): class Meta: model = models.Permission # fields = '__all__' fields = ['title', 'url', 'name', 'parent', 'menu'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) # 批量权限使用的form class MultiPermissionForm(forms.ModelForm): class Meta: model = models.Permission fields = ['title', 'url', 'name', 'parent', 'menu'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields: self.fields[field].widget.attrs.update({"class": "form-control"}) self.fields['parent'].choices = [(None, '-------')] + list( models.Permission.objects.filter(parent__isnull=True).exclude( menu__isnull=True).values_list('id', 'title')) def clean(self): menu = self.cleaned_data.get('menu') pid = self.cleaned_data.get('parent') if menu and pid: raise forms.ValidationError('菜单和根权限同时只能选择一个') return self.cleaned_data
from django.shortcuts import render, HttpResponse, redirect, reverse from rbac import models from rbac.forms import * from django.db.models import Q from rbac.server.routes import get_all_url_dict def role_list(request): all_roles = models.Role.objects.all() return render(request, 'rbac/role_list.html', {"all_roles": all_roles}) def role(request, edit_id=None): obj = models.Role.objects.filter(id=edit_id).first() form_obj = RoleForm(instance=obj) if request.method == 'POST': form_obj = RoleForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:role_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj}) def del_role(request, del_id): models.Role.objects.filter(id=del_id).delete() return redirect(reverse('rbac:role_list')) # 菜单信息 权限信息 def menu_list(request): all_menu = models.Menu.objects.all() mid = request.GET.get('mid') if mid: permission_query = models.Permission.objects.filter(Q(menu_id=mid) | Q(parent__menu_id=mid)) else: permission_query = models.Permission.objects.all() all_permission = permission_query.values('id', 'url', 'title', 'name', 'menu_id', 'parent_id', 'menu__title') all_permission_dict = {} for item in all_permission: menu_id = item.get('menu_id') if menu_id: item['children'] = [] all_permission_dict[item['id']] = item for item in all_permission: pid = item.get('parent_id') if pid: all_permission_dict[pid]['children'].append(item) print(all_permission_dict) return render(request, 'rbac/menu_list.html', {"all_menu": all_menu, 'all_permission_dict': all_permission_dict, 'mid': mid}) def menu(request, edit_id=None): obj = models.Menu.objects.filter(id=edit_id).first() form_obj = MenuForm(instance=obj) if request.method == 'POST': form_obj = MenuForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:menu_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj}) def permission(request, edit_id=None): obj = models.Permission.objects.filter(id=edit_id).first() form_obj = PermissionForm(instance=obj) if request.method == 'POST': form_obj = PermissionForm(request.POST, instance=obj) if form_obj.is_valid(): form_obj.save() return redirect(reverse('rbac:menu_list')) return render(request, 'rbac/form.html', {'form_obj': form_obj}) def del_permission(request, del_id): models.Permission.objects.filter(id=del_id).delete() return redirect(reverse('rbac:menu_list')) from django.forms import modelformset_factory, formset_factory def multi_permissions(request): """ 批量操作权限 :param request: :return: """ post_type = request.GET.get('type') # 更新和编辑用的 FormSet = modelformset_factory(models.Permission, MultiPermissionForm, extra=0) # 增加用的 AddFormSet = formset_factory(MultiPermissionForm, extra=0) permissions = models.Permission.objects.all() # 获取路由系统中所有URL router_dict = get_all_url_dict(ignore_namespace_list=['admin', 'rbac']) # 数据库中的所有权限的别名 permissions_name_set = set([i.name for i in permissions]) # 路由系统中的所有权限的别名 router_name_set = set(router_dict.keys()) if request.method == 'POST' and post_type == 'add': add_formset = AddFormSet(request.POST) if add_formset.is_valid(): print(add_formset.cleaned_data) permission_obj_list = [models.Permission(**i) for i in add_formset.cleaned_data] query_list = models.Permission.objects.bulk_create(permission_obj_list) for i in query_list: permissions_name_set.add(i.name) add_name_set = router_name_set - permissions_name_set add_formset = AddFormSet(initial=[row for name, row in router_dict.items() if name in add_name_set]) del_name_set = permissions_name_set - router_name_set del_formset = FormSet(queryset=models.Permission.objects.filter(name__in=del_name_set)) update_name_set = permissions_name_set & router_name_set update_formset = FormSet(queryset=models.Permission.objects.filter(name__in=update_name_set)) if request.method == 'POST' and post_type == 'update': update_formset = FormSet(request.POST) if update_formset.is_valid(): update_formset.save() update_formset = FormSet(queryset=models.Permission.objects.filter(name__in=update_name_set)) return render( request, 'rbac/multi_permissions.html', { 'del_formset': del_formset, 'update_formset': update_formset, 'add_formset': add_formset, } ) def distribute_permissions(request): """ 分配权限 :param request: :return: """ uid = request.GET.get('uid') rid = request.GET.get('rid') if request.method == 'POST' and request.POST.get('postType') == 'role': user = models.User.objects.filter(id=uid).first() if not user: return HttpResponse('用户不存在') user.roles.set(request.POST.getlist('roles')) if request.method == 'POST' and request.POST.get('postType') == 'permission' and rid: role = models.Role.objects.filter(id=rid).first() if not role: return HttpResponse('角色不存在') role.permissions.set(request.POST.getlist('permissions')) # 所有用户 user_list = models.User.objects.all() user_has_roles = models.User.objects.filter(id=uid).values('id', 'roles') # print(user_has_roles) user_has_roles_dict = {item['roles']: None for item in user_has_roles} """ 用户拥有的角色id user_has_roles_dict = { 角色id:None } """ role_list = models.Role.objects.all() if rid: role_has_permissions = models.Role.objects.filter(id=rid).values('id', 'permissions') elif uid and not rid: user = models.User.objects.filter(id=uid).first() if not user: return HttpResponse('用户不存在') role_has_permissions = user.roles.values('id', 'permissions') else: role_has_permissions = [] print(role_has_permissions) role_has_permissions_dict = {item['permissions']: None for item in role_has_permissions} """ 角色拥有的权限id role_has_permissions_dict = { 权限id:None } """ all_menu_list = [] queryset = models.Menu.objects.values('id', 'title') menu_dict = {} """ all_menu_list = [ { id: title : , children : [ { 'id', 'title', 'menu_id', 'children: [ 'id', 'title', 'parent_id' ] } ] }, {'id': None, 'title': '其他', 'children': [ {'id', 'title', 'parent_id'}]} ] menu_dict = { 菜单的ID: { id: title : , children : [ { 'id', 'title', 'menu_id', 'children: [ 'id', 'title', 'parent_id' ] } ] }, none:{'id': None, 'title': '其他', 'children': [ {'id', 'title', 'parent_id'}]} } """ for item in queryset: item['children'] = [] # 放二级菜单,父权限 menu_dict[item['id']] = item all_menu_list.append(item) other = {'id': None, 'title': '其他', 'children': []} all_menu_list.append(other) menu_dict[None] = other root_permission = models.Permission.objects.filter(menu__isnull=False).values('id', 'title', 'menu_id') root_permission_dict = {} """ root_permission_dict = { 父权限的id : { 'id', 'title', 'menu_id', 'children: [ { 'id', 'title', 'parent_id' } ] }} """ for per in root_permission: per['children'] = [] # 放子权限 nid = per['id'] menu_id = per['menu_id'] root_permission_dict[nid] = per menu_dict[menu_id]['children'].append(per) node_permission = models.Permission.objects.filter(menu__isnull=True).values('id', 'title', 'parent_id') for per in node_permission: pid = per['parent_id'] if not pid: menu_dict[None]['children'].append(per) continue root_permission_dict[pid]['children'].append(per) return render( request, 'rbac/distribute_permissions.html', { 'user_list': user_list, 'role_list': role_list, 'user_has_roles_dict': user_has_roles_dict, 'role_has_permissions_dict': role_has_permissions_dict, 'all_menu_list': all_menu_list, 'uid': uid, 'rid': rid } )
权限的初始化
from django.conf import settings def init_permission(request, user): # 1. 查当前登录用户拥有的权限 permission_query = user.roles.filter(permissions__url__isnull=False).values( 'permissions__url', 'permissions__title', 'permissions__id', 'permissions__name', 'permissions__parent_id', 'permissions__parent__name', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', 'permissions__menu__weight', ).distinct() # 存放权限信息 permission_dict = {} # 存放菜单信息 menu_dict = {} for item in permission_query: permission_dict[item['permissions__name']] = {'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id'], 'pname': item['permissions__parent__name'], 'title': item['permissions__title']} menu_id = item.get('permissions__menu_id') if not menu_id: continue if menu_id not in menu_dict: menu_dict[menu_id] = { 'title': item['permissions__menu__title'], 'icon': item['permissions__menu__icon'], 'weight': item['permissions__menu__weight'], 'children': [ {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']} ] } else: menu_dict[menu_id]['children'].append( {'title': item['permissions__title'], 'url': item['permissions__url'], 'id': item['permissions__id'], 'pid': item['permissions__parent_id']}) # # 2. 将权限信息写入到session request.session[settings.PERMISSION_SESSION_KEY] = permission_dict # 将菜单信息写入到session request.session[settings.MENU_SESSION_KEY] = menu_dict
from django.conf import settings from django.utils.module_loading import import_string from django.urls import RegexURLResolver, RegexURLPattern from collections import OrderedDict def recursion_urls(pre_namespace, pre_url, urlpatterns, url_ordered_dict): for item in urlpatterns: if isinstance(item, RegexURLResolver): if pre_namespace: if item.namespace: namespace = "%s:%s" % (pre_namespace, item.namespace,) else: namespace = pre_namespace else: if item.namespace: namespace = item.namespace else: namespace = None recursion_urls(namespace, pre_url + item.regex.pattern, item.url_patterns, url_ordered_dict) else: if pre_namespace: name = "%s:%s" % (pre_namespace, item.name,) else: name = item.name if not item.name: raise Exception('URL路由中必须设置name属性') url = pre_url + item._regex url_ordered_dict[name] = {'name': name, 'url': url.replace('^', '').replace('$', '')} def get_all_url_dict(ignore_namespace_list=None): """ 获取路由中 :return: """ ignore_list = ignore_namespace_list or [] url_ordered_dict = OrderedDict() md = import_string(settings.ROOT_URLCONF) urlpatterns = [] for item in md.urlpatterns: if isinstance(item, RegexURLResolver) and item.namespace in ignore_list: continue urlpatterns.append(item) recursion_urls(None, "/", urlpatterns, url_ordered_dict) return url_ordered_dict
中间件的校验
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import HttpResponse import re class PermissionMiddleware(MiddlewareMixin): def process_request(self, request): # 对权限进行校验 # 1. 当前访问的URL current_url = request.path_info # 白名单的判断 for i in settings.WHITE_URL_LIST: if re.match(i, current_url): return # 2. 获取当前用户的所有权限信息 permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY) request.breadcrumb_list = [ {"title": '首页', 'url': '#'}, ] # 3. 权限的校验 print(permission_dict) for item in permission_dict.values(): url = item['url'] if re.match("^{}$".format(url), current_url): pid = item['pid'] id = item['id'] pname = item['pname'] if pid: # 表示当前权限是子权限,让父权限是展开 request.current_menu_id = pid request.breadcrumb_list.extend( [{"title": permission_dict[pname]['title'], 'url': permission_dict[pname]['url']}, {"title": item['title'], 'url': item['url']}] ) else: # 表示当前权限是父权限,要展开的二级菜单 request.current_menu_id = id # 添加面包屑导航 request.breadcrumb_list.append({"title": item['title'], 'url': item['url']}) return else: return HttpResponse('没有权限')
自定义inclusion_tag,simple_tag,filter
from django import template register = template.Library() from django.conf import settings import re from collections import OrderedDict @register.inclusion_tag('rbac/menu.html') def menu(request): menu_dict = request.session.get(settings.MENU_SESSION_KEY) order_dict = OrderedDict() for key in sorted(menu_dict, key=lambda x: menu_dict[x]['weight'], reverse=True): order_dict[key] = menu_dict[key] item = order_dict[key] item['class'] = 'hide' for i in item['children']: if i['id'] == request.current_menu_id: i['class'] = 'active' item['class'] = '' return {"menu_list": order_dict} @register.inclusion_tag('rbac/breadcrumb.html') def breadcrumb(request): return {'breadcrumb_list': request.breadcrumb_list} @register.filter def has_permission(request, permission): if permission in request.session.get(settings.PERMISSION_SESSION_KEY): return True @register.simple_tag def gen_role_url(request, rid): params = request.GET.copy() params._mutable = True params['rid'] = rid return params.urlencode()
templates
<ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> {% for li in breadcrumb_list %} {% if forloop.last %} <li>{{ li.title }}</li> {% else %} <li><a href="{{ li.url }}">{{ li.title }}</a></li> {% endif %} {% endfor %} </ol>
{% extends 'layout.html' %} {% load rbac %} {% block css %} <style> .user-area ul { padding-left: 20px; } .user-area li { cursor: pointer; padding: 2px 0; } .user-area li a { display: block; } .user-area li.active { font-weight: bold; color: red; } .user-area li.active a { color: red; } .role-area tr td a { display: block; } .role-area tr.active { background-color: #f1f7fd; border-left: 3px solid #fdc00f; } .permission-area tr.root { background-color: #f1f7fd; cursor: pointer; } .permission-area tr.root td i { margin: 3px; } .permission-area .node { } .permission-area .node input[type='checkbox'] { margin: 0 5px; } .permission-area .node .parent { padding: 5px 0; } .permission-area .node label { font-weight: normal; margin-bottom: 0; font-size: 12px; } .permission-area .node .children { padding: 0 0 0 20px; } .permission-area .node .children .child { display: inline-block; margin: 2px 5px; } table { font-size: 12px; } .panel-body { font-size: 12px; } .panel-body .form-control { font-size: 12px; } </style> {% endblock %} {% block content %} <div class="luffy-container"> <div class="col-md-3 user-area"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-address-book-o" aria-hidden="true"></i> 用户信息 </div> <div class="panel-body"> <ul> {% for user in user_list %} <li class= {% if user.id|safe == uid %} "active" {% endif %}> <a href="?uid={{ user.id }}">{{ user.name }}</a></li> {% endfor %} </ul> </div> </div> </div> <div class="col-md-3 role-area"> <form method="post"> {% csrf_token %} <input type="hidden" name="postType" value="role"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-book" aria-hidden="true"></i> 角色 {% if uid %} <button type="submit" class="right btn btn-success btn-xs" style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-save" aria-hidden="true"></i> 保存 </button> {% endif %} </div> <div class="panel-body" style="color: #d4d4d4;padding:10px 5px;"> 提示:点击用户后才能为其分配角色 </div> <table class="table"> <thead> <tr> <th>角色</th> <th>选择</th> </tr> </thead> <tbody> {% for role in role_list %} <tr {% if role.id|safe == rid %} class="active" {% endif %}> <td><a href="?{% gen_role_url request role.id %}">{{ role.name }}</a></td> <td> {% if role.id in user_has_roles_dict %} <input type="checkbox" name="roles" value="{{ role.id }}" checked/> {% else %} <input type="checkbox" name="roles" value="{{ role.id }}"/> {% endif %} </td> </tr> {% endfor %} </tbody> </table> </div> </form> </div> <div class="col-md-6 permission-area"> <form method="post"> {% csrf_token %} <input type="hidden" name="postType" value="permission"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-sitemap" aria-hidden="true"></i> 权限分配 {% if rid %} <button class="right btn btn-success btn-xs" style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-save" aria-hidden="true"></i> 保存 </button> {% endif %} </div> <div class="panel-body" style="color: #d4d4d4;padding: 10px 5px;"> 提示:点击角色后,才能为其分配权限。 </div> <table class="table"> <tbody> {% for item in all_menu_list %} <tr class="root"> <td><i class="fa fa-caret-down" aria-hidden="true"></i>{{ item.title }}</td> </tr> <tr class="node"> <td> {% for node in item.children %} <div class="parent"> {% if node.id in role_has_permissions_dict %} <input id="permission_{{ node.id }}" name="permissions" value="{{ node.id }}" type="checkbox" checked> {% else %} <input id="permission_{{ node.id }}" name="permissions" value="{{ node.id }}" type="checkbox"> {% endif %} <label for="permission_{{ node.id }}">{{ node.title }}</label> </div> <div class="children"> {% for child in node.children %} <div class="child"> {% if child.id in role_has_permissions_dict %} <input id="permission_{{ child.id }}" name="permissions" type="checkbox" value="{{ child.id }}" checked> {% else %} <input id="permission_{{ child.id }}" name="permissions" type="checkbox" value="{{ child.id }}"> {% endif %} <label for="permission_{{ child.id }}">{{ child.title }}</label> </div> {% endfor %} </div> {% endfor %} </td> </tr> {% endfor %} </tbody> </table> </div> </form> </div> </div> {% endblock %} {% block js %} <script> $(function () { bindRootPermissionClick(); }); function bindRootPermissionClick() { $('.permission-area').on('click', '.root', function () { var caret = $(this).find('i'); if (caret.hasClass('fa-caret-right')) { caret.removeClass('fa-caret-right').addClass('fa-caret-down'); $(this).next().removeClass('hide'); } else { caret.removeClass('fa-caret-down').addClass('fa-caret-right'); $(this).next().addClass('hide'); } }) } </script> {% endblock %}
{% extends 'layout.html' %} {% block css %} <style> ul { list-style-type: none; padding: 0; } ul li { float: left; padding: 10px; padding-left: 0; width: 80px; } ul li i { font-size: 18px; margin-left: 5px; color: #6d6565; } </style> {% endblock %} {% block content %} <form class="form-horizontal" novalidate method="post" action="" style="margin-top: 50px;width: 95%"> {% csrf_token %} {% for field in form_obj %} <div class="form-group {% if field.errors %}has-error{% endif %} "> <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label> <div class="col-sm-6"> {{ field }} </div> <span class="help-block">{{ field.errors.0 }}</span> </div> {% endfor %} <div class="form-group"> <div class="col-sm-offset-2 col-sm-6"> <button type="submit" class="btn btn-default">提交</button> </div> </div> </form> {% endblock %}
{% extends 'layout.html' %} {% block css %} <style> .permission-area tr.parent { background-color: #cae7fd;; } .menu-body tr.active { background-color: #f1f7fd; border-left: 3px solid #fdc00f; } </style> {% endblock %} {% block content %} <div style="margin: 20px"> <div class="col-sm-3"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><i class="fa fa-book"></i> 菜单管理 <a href="{% url 'rbac:menu_add' %}" class="btn btn-success btn-sm pull-right " style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-plus"></i> 新建</a> </div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>图标</th> <th>操作</th> </tr> </thead> <tbody class="menu-body"> {% for menu in all_menu %} <tr class=" {% if menu.id|safe == mid %} active {% endif %} "> <td><a href="?mid={{ menu.id }}">{{ menu.title }}</a></td> <td><i class="fa {{ menu.icon }} "></i></td> <td> <a href="{% url 'rbac:menu_edit' menu.id %}"> <i class="fa fa-edit"></i> </a> <a href=""> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> </div> <div class="col-sm-9"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><i class="fa fa-cubes"></i> 权限管理 <a href="{% url 'rbac:multi_permissions' %}" class="btn btn-primary btn-sm pull-right " style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-scissors"></i> 批量操作</a> <a href="{% url 'rbac:permission_add' %}" class="btn btn-success btn-sm pull-right " style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-plus"></i> 新建</a> </div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>URL</th> <th>URL别名</th> <th>菜单</th> <th>所属菜单</th> <th>操作</th> </tr> </thead> <tbody class="permission-area"> {% for p_permission in all_permission_dict.values %} <tr class="parent" id="{{ p_permission.id }}"> <td class="title"> <i class="fa fa-caret-down"></i> {{ p_permission.title }} </td> <td>{{ p_permission.url }}</td> <td>{{ p_permission.name }}</td> <td> 是 </td> <td> {{ p_permission.menu__title }} </td> <td> <a href="{% url 'rbac:permission_edit' p_permission.id %}"> <i class="fa fa-edit"></i> </a> <a href="{% url 'rbac:permission_del' p_permission.id %}"> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% for c_permission in p_permission.children %} <tr pid="{{ c_permission.parent_id }}"> <td>{{ c_permission.title }}</td> <td>{{ c_permission.url }}</td> <td>{{ c_permission.name }}</td> <td></td> <td></td> <td> <a href="{% url 'rbac:permission_edit' c_permission.id %}"> <i class="fa fa-edit"></i> </a> <a href="{% url 'rbac:permission_del' c_permission.id %}"> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} {% endfor %} </tbody> </table> </div> </div> </div> {% endblock %} {% block js %} <script> $('.permission-area').on('click', '.parent .title', function () { var caret = $(this).find('i'); var id = $(this).parent().attr('id'); if (caret.hasClass('fa-caret-right')) { caret.removeClass('fa-caret-right').addClass('fa-caret-down'); $(this).parent().nextAll('tr[pid="' + id + '"]').removeClass('hide'); } else { caret.removeClass('fa-caret-down').addClass('fa-caret-right'); $(this).parent().nextAll('tr[pid="' + id + '"]').addClass('hide'); } }) </script> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div style="margin: 20px"> <h1>角色管理</h1> <a href="{% url "rbac:role_add" %}" class="btn btn-success">添加</a> <table class="table table-bordered table-hover" style="margin-top: 5px"> <thead> <tr> <th>序号</th> <th>名称</th> <th>操作</th> </tr> </thead> <tbody> {% for role in all_roles %} <tr> <td>{{ forloop.counter }}</td> <td>{{ role.name }}</td> <td> <a href="{% url 'rbac:role_edit' role.id %}"> <i class="fa fa-edit"></i> </a> <a href="{% url 'rbac:role_del' role.id %}"> <i class="fa fa-trash-o"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form method="post" action="?type=add"> {% csrf_token %} {{ add_formset.management_form }} <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-binoculars" aria-hidden="true"></i> 待新建权限列表 <button class="right btn btn-primary btn-xs" style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-save" aria-hidden="true"></i> 新建 </button> </div> <div class="panel-body" style="color: #9d9d9d;"> 注意:路由系统中自动发现且数据库中不存在的路由。 </div> <table class="table table-bordered"> <thead> <tr> <th>序号</th> <th>名称</th> <th>URL</th> <th>别名</th> <th>所属菜单</th> <th>根权限</th> </tr> </thead> <tbody> {% for form in add_formset %} <tr> <td style="vertical-align: middle;">{{ forloop.counter }}</td> <td>{{ form.title }} <span>{{ form.title.errors.0 }}</span></td> <td>{{ form.url }}</td> <td>{{ form.name }}</td> <td>{{ form.parent }}</td> <td>{{ form.menu }}</td> </tr> {% endfor %} </tbody> </table> </div> </form> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-th-list" aria-hidden="true"></i> 待删除权限列表 </div> <div class="panel-body" style="color: #9d9d9d;"> 注意:数据库中存在,但路由系统中不存在的路由。 </div> <table class="table table-bordered"> <thead> <tr> <th>序号</th> <th>名称</th> <th>URL</th> <th>别名</th> <th>父权限</th> <th>所属菜单</th> <th>操作</th> </tr> </thead> <tbody> {% for form in del_formset %} <tr> {{ form.id }} <td style="vertical-align: middle;">{{ forloop.counter }}</td> <td>{{ form.title }} <span>{{ form.title.errors.0 }}</span></td> <td>{{ form.url }}</td> <td>{{ form.name }}</td> <td>{{ form.parent }}</td> <td>{{ form.menu }}</td> <td> <a href="{% url 'rbac:permission_del' form.id.value %}" style="color:#d9534f;"> <i class="fa fa-trash-o" aria-hidden="true"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> <form method="post" action="?type=update"> {% csrf_token %} {{ update_formset.management_form }} <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-sitemap" aria-hidden="true"></i> 待更新权限列表 <button class="right btn btn-primary btn-xs" style="padding: 2px 8px;margin: -3px;"> <i class="fa fa-save" aria-hidden="true"></i> 更新 </button> </div> <div class="panel-body" style="color: #9d9d9d;"> 注意:数据库和路由系统都存在的路由。 </div> <table class="table table-bordered"> <thead> <tr> <th>序号</th> <th>名称</th> <th>URL</th> <th>别名</th> <th>父权限</th> <th>所属菜单</th> <th>操作</th> </tr> </thead> <tbody> {% for form in update_formset %} <tr> {{ form.id }} <td style="vertical-align: middle;">{{ forloop.counter }}</td> <td>{{ form.title }} <span>{{ form.title.errors.0 }}</span></td> <td>{{ form.url }}</td> <td>{{ form.name }}</td> <td>{{ form.parent }}</td> <td>{{ form.menu }}</td> <td> <a href="{% url 'rbac:permission_del' form.id.value %}" style="color:#d9534f;"> <i class="fa fa-trash-o" aria-hidden="true"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> </form> </div> {% endblock %}
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>爱软测</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} "> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'css/menu.css' %}"> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } </style> {% block css %} {% endblock %} </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">爱软测 </span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">软测管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% load rbac %} {# {% menu request %}#} </div> </div> <div class="right-body"> <div> {# {% breadcrumb request %}#} </div> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="{% static 'js/menu.js' %} "></script> {% block js %} {% endblock %} </body> </html>
幻想毫无价值,计划渺如尘埃,目标不可能达到。这一切的一切毫无意义——除非我们付诸行动。