将多种权限合并到一个组中, 分配时即可一次性分配多种权限, 例如, 售后
具有某几种权限, 当有成员被配置为他时就会拥有着几种权限
首先咱们来实现权限分组列表, 目的是展示所有的权限组, 同样需要添加一些权限组
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /admin/groups/ |
参数格式 | 无参数 |
返回结果
html
# 在myadmin/views.py中定义如下视图
class GroupListView(View):
"""
分组列表视图
url:/admin/groups/
"""
def get(self, request):
groups = Group.objects.only('name').all()
return render(request, 'myadmin/group/group_list.html', context={'groups': groups})
# 在myadmin/urls.py中添加如下路由
path('groups/', views.GroupsView.as_view(), name='group_list')
可以先写一个简单的模板, 然后运行到前端看一下
{% extends 'myadmin/base/content_base.html' %}
{% load static %}
{% load news_template_filters %} {# 分页过滤器 #}
{% block page_header %}
系统设置
{% endblock %}
{% block page_option %}
分组管理
{% endblock %}
首先来到菜单管理创建一个名字为分组管理
的菜单路由地址是group_list
然后刷新页面即可看到我们的分组管理页面
接下来我们来填充它的内容, 修改group_list.html模板
{% extends 'admin/content_base.html' %}
{% load static %}
{% load news_template_filters %} {# 分页过滤器 #}
{% block page_header %}
系统设置
{% endblock %}
{% block page_option %}
权限分组
{% endblock %}
{% block content %}
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">分组列表h3>
<div class="box-tools">
div>
div>
<div class="box-body">
<div style="margin-bottom: 10px">
div>
<table class="table table-bordered">
<tbody>
<tr>
<th>#th>
<th>组名th>
<th>菜单th>
tr>
{% for group in groups %}
<tr>
<td style="width: 40px" data-url="#"><a
href="#">{{ forloop.counter }}a>td>
<td>{{ group.name }}td>
<td>
{% for permis in group.permissions.all %}
{{ permis.name }}/
{% empty %}
暂未分配权限
{% endfor %}
td>
tr>
{% endfor %}
tbody>
table>
div>
div>
{% endblock %}
前端页面写好了, 但是我们的权限分组仍然只是一个空列表, 我们可以先使用sql语句添加一些数据, 为后续的详情页做铺垫
运行添加分组的语句(例)
INSERT INTO auth_groups(name) VALUES('售后');
运行添加分组权限的语句(例)
INSERT INTO auth_group_permissions(group_id, permission_id) VALUES(1, 70);
对应的权限码你可以在auth_permission
表中看到, 选择我们在菜单管理中创建的
添加完成后的效果
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /admin/group/ |
参数格式 | 路径参数 |
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
group_id | 整数 | 是 | 分组id |
返回数据
返回html表单
# 在myadmin/views.py中添加如下视图
class GroupUpdateView(View):
"""
分组更新视图
url: /admin/group//
"""
def get(self, request, group_id):
# 1. 获取被修改的组对象
group = Group.objects.filter(id=group_id).first()
# 2. 判断对象是否存在
if not group:
# 2.1 如果没有就报错
return json_response(errno=Code.NODATA, errmsg='没有此分组!')
# 3. 创建到表单对象
form = GroupModelForm(instance=group)
# 4 返回渲染html
return render(request, 'myadmin/group/group_detail.html')
# 在myadmin/views.py中添加如下视图
class GroupUdateView(View):
"""
分组更新视图
url: /admin/group//
"""
def get(self, request, group_id):
# 1. 获取被修改的组对象
group = Group.objects.filter(id=group_id).first()
# 2. 判断对象是否存在
if not group:
# 2.1 如果没有就报错
return json_response(errno=Code.NODATA, errmsg='没有此分组!')
# 3. 创建到表单对象
form = GroupModelForm(instance=group)
# 4. 拿到所有可用的一级菜单
menus = Menu.objects.only('name', 'permission_id').select_related('permission').filter(is_delete=False,parent=None)
# 5. 拿到当前组的可用权限
permissions = group.permissions.only('id').all()
# 6. 返回渲染html
return render(request, 'myadmin/group/group_detail.html', context={
'form': form,
'menus': menus,
'permissions': permissions
})
# 在admin/urls.py中添加如下路由
path('group//' , views.GroupUdateView.as_view(), name='update_group')
class GroupModelForm(forms.ModelForm):
#permissions = forms.ModelMultipleChoiceField(queryset=None, required=False, help_text='权限', label='权限')
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.fields['permissions'].queryset = Permission.objects.filter('menu__is_delete=False')
# 上面这样写的作用: 由于我们的权限表和菜单表是一对一关系, 如果菜单设置的是逻辑删除, 展示在分组详情页上权限项的就必须是没有被逻辑删除的对象
# 如果菜单设置的是真实删除, 那就不需要上面这一串代码
class Meta:
model = Group
fields = ['name', 'permissions']
{% extends 'myadmin/base/content_base.html' %}
{% load static %}
{% load admin_customer_tags %}
{% block page_header %}
系统设置
{% endblock %}
{% block page_option %}
权限分组
{% endblock %}
{% block content %}
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">分组详情h3>
div>
<div class="box-body">
<div class="row">
<div class="col-md-3">div>
<div class="col-md-6">
<form class="form-horizontal">
{% csrf_token %}
{% for field in form %}
{% if field.name == 'permissions' %}
<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-10">
{% for error in field.errors %}
<label class="control-label"
for="{{ field.id_for_label }}">{{ error }}label>
{% endfor %}
{% for menu in menus %}
<div class="row" style="margin: 0">
<div class="checkbox">
<label for="menu_{{ menu.permission.id }}">
<input {% if menu.permission in permissions %}checked{% endif %} type="checkbox" name="permissions" id="menu_{{ menu.permission.id }}"
value="{{ menu.permission.id }}">{{ menu.name }}
label>
div>
{% for child in menu.children.all %}
<div class="checkbox col-sm-offset-1">
<label for="menu_{{ child.permission.id }}">
<input type="checkbox" {% if child.permission in permissions %}checked{% endif %} name="permissions" id="menu_{{ child.permission.id }}"
value="{{ child.permission.id }}">{{ child.name }}
label>
div>
{% endfor %}
div>
{% endfor %}
div>
div>
{% else %}
<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-10">
{% for error in field.errors %}
<label class="control-label"
for="{{ field.id_for_label }}">{{ error }}label>
{% endfor %}
{% add_class field 'form-control' %}
div>
div>
{% endif %}
{% endfor %}
form>
div>
<div class="col-md-3">div>
div>
div>
<div class="box-footer">
<button type="button" class="btn btn-default back">返回button>
<button type="button" data-url="{% url 'myadmin:group_update' form.instance.id %}"
class="btn btn-primary pull-right save">保存
button>
div>
div>
{% endblock %}
记得修改一下group_list.html
中的data_url
属性, 他的目的是让我们可以点击编号跳转到权限详情页
<td style="width: 40px" data-url="{% url 'myadmin:update_group' group.id %}">
<a href="#">{{ forloop.counter }}a>
td>
// 创建 js/myadmin/group/group_list.js
$(()=>{
// 分组详情
$('tr').each(function () {
$(this).children('td:first').click(function () {
$('#content').load(
$(this).data('url'),
(response, status, xhr) => {
if (status !== 'success') {
message.showError('服务器超时,请重试!')
}
}
);
})
});
});
记得再group_list.html中引用
{% block script %}
<script src="{% static 'js/myadmin/group/group_list.js'%}">script>
{% endblock %}
类目 | 说明 |
---|---|
请求方法 | PUT |
url定义 | /admin/group/ |
参数格式 | 路径参数+表单参数 |
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
group_id | 整数 | 是 | 分组id |
name | 字符串 | 是 | 分组名称 |
permissions | 整数 | 否 | 权限id |
返回数据
# 修改正常返回json数据
{
"errno": "0",
"errmsg": "用户修改成功!"
}
如果有错误,返回html表单
# 在myadmin/views.py的GroupUpdateView视图中添加put方法
class GroupUpdateView(View):
"""
分组更新视图
url:/admin/group//
"""
def put(self, request, group_id):
# 1. 拿到需要修改的分组
group = Group.objects.filter(id=group_id).first()
# 1.1 判断分组是否存在
if not group:
return json_response(errno=Code.NODATA, errmsg='没有此分组!')
# 2. 拿到前端传递的参数
put_data = QueryDict(request.body)
# 3. 校验参数
# 3.1 获取表单对象, 通过group作为标识寻找匹配的数据, 再与put_data比较, 并修改不同处
form = GroupModelForm(put_data, instance=group)
# 3.2 表单校验
if form.is_valid():
# 4. 校验成功, 保存数据
form.save()
return json_response(errmsg='分组修改成功!')
else:
# 4. 拿到所有可用的一级菜单
menus = Menu.objects.only('name', 'permission_id').select_related('permission').filter(is_delete=False,parent=None)
# 5. 拿到当前组的可用权限
permissions = group.permissions.only('id').all()
# 6. 返回渲染html
return render(request, 'myadmin/group/group_detail.html', context={
'form': form,
'menus': menus,
'permissions': permissions
})
# 创建 js/admin/group/group_detail.js
$(() => {
// 返回按钮
$('.box-footer button.back').click(() => {
$('#content').load(
$('.sidebar-menu li.active a').data('url'),
(response, status, xhr) => {
if (status !== 'success') {
message.showError('服务器超时,请重试!')
}
}
);
});
// 保存按钮
$('.box-footer button.save').click(function () {
// 将表单中的数据进行格式化
$
.ajax({
url: $(this).data('url'),
data: $('form').serialize(),
type: 'PUT'
})
.done((res) => {
if (res.errno === '0') {
message.showSuccess('修改分组成功!');
$('#content').load(
$('.sidebar-menu li.active a').data('url'),
(response, status, xhr) => {
if (status !== 'success') {
message.showError('服务器超时,请重试!')
}
}
);
} else {
$('#content').html(res)
}
})
.fail((res) => {
message.showError('服务器超时,请重试!')
})
});
// 复选框逻辑
// 点击一级菜单,二级菜单联动
// 注意要在一级菜单中class属性中加上one,二级菜单中加上two
$('div.checkbox.one').each(function () {
let $this = $(this);
$this.find(':checkbox').click(function () {
if($(this).is(':checked')){
$this.siblings('div.checkbox.two').find(':checkbox').prop('checked', true)
}else{
$this.siblings('div.checkbox.two').find(':checkbox').prop('checked', false)
}
})
});
// 点击二级菜单,一级菜单联动
$('div.checkbox.two').each(function () {
let $this = $(this);
$this.find(':checkbox').click(function () {
if($(this).is(':checked')){
$this.siblings('div.checkbox.one').find(':checkbox').prop('checked', true)
}else {
if(!$this.siblings('div.checkbox.two').find(':checkbox').is(':checked')){
$this.siblings('div.checkbox.one').find(':checkbox').prop('checked', false)
}
}
})
});
});
前端模板添加两个属性one
和two
, 分别代表一级和二级菜单
{% for menu in menus %}
{% for child in menu.children.all %}
{% endfor %}
{% endfor %}
// 复选框逻辑
// 点击一级菜单,二级菜单联动
// 注意要在一级菜单中class属性中加上one,二级菜单中加上two
// 给带有one属性的所有checkbox加上这个函数
$('div.checkbox.one').each(function () {
let $this = $(this);
$this.find(':checkbox').click(function () {
if ($(this).is(':checked')) {
// 如果点击checkbox是让其被选中,
// 则让其所有的子选项全部选中
$this.siblings('div.checkbox.two').find(':checkbox').prop('checked', true)
} else {
// 否则代表取消选中checkbox,
// 则其所有的子选项也全部取消
$this.siblings('div.checkbox.two').find(':checkbox').prop('checked', false)
}
})
});
// 点击二级菜单,一级菜单联动
// 给带有two属性的所有checkbox加上这个函数
$('div.checkbox.two').each(function () {
let $this = $(this);
$this.find(':checkbox').click(function () {
if ($(this).is(':checked')) {
// 如果点击checkbox是让其被checked,
// 则检查其他子选项是否还有unchecked的
if (!$this.siblings('div.checkbox.two').find(':checkbox').is(':checked')) {
// 如果有,就让父选项变成indeterminate(不确定的)状态
$this.siblings('div.checkbox.one').find(':checkbox').prop('indeterminate', true)
}else{
// 如果全都被checked,就移出父选项的indeterminate状态,
$this.siblings('div.checkbox.one').find(':checkbox').prop('indeterminate', false);
// 然后让父选项被checked
$this.siblings('div.checkbox.one').find(':checkbox').prop('checked', true)
}
} else {
// 否则代表取消选中checkbox,
// 则判断所有的子选项中是否仍有被checked的
if ($this.siblings('div.checkbox.two').find(':checkbox').is(':checked')) {
// 如果有, 就让父选项变成indeterminate(不确定的)状态
$this.siblings('div.checkbox.one').find(':checkbox').prop('indeterminate', true)
}else {
// 如果全都被checked,就移出父选项的indeterminate状态,
$this.siblings('div.checkbox.one').find(':checkbox').prop('indeterminate', false);
// 然后移出父选项的checked状态
$this.siblings('div.checkbox.one').find(':checkbox').prop('checked', false)
}
}
})
});
参考文档:如何实现checkbox的第三种状态?
IIII. 添加分组页面
1>接口设计
1.1>接口说明:
类目
说明
请求方法
GET
url定义
/admin/group/
参数格式
无参数
1.2>返回数据
返回html表单
2>后端代码
2.1>视图
# 在admin/views.py中添加如下视图
class GroupAddView(View):
"""
添加分组视图
url: /admin/group/
"""
def get(self, request):
# 1. 创建表单模型对象
form = GroupModelForm()
# 2. 拿到所有可用的一级菜单
menus = Menu.objects.only('name', 'permission_id').select_related('permission').filter(is_delete=False, parent=None)
# 3. 返回表单渲染的html
return render(request, 'myadmin/group/group_detail.html', context={
'form': form,
'menus': menus
})
2.2>路由
# 在myadmin/urls.py中添加如下路由
path('add_group/', views.GroupAddView.as_view(), name='add_group'),
3>前端代码
3.1>html
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm"
data-url="{% url 'myadmin:add_group' %}">添加分组
button>
div>
3.2>js
// 在myadmin/group/group_list.js 中添加 添加group的按钮的js代码如下
// 添加分组
$('.box-tools button').click(function () {
$('#content').load(
$(this).data('url'),
(response, status, xhr) => {
if (status !== 'success') {
message.showError('服务器超时,请重试!')
}
}
);
});
V. 添加分组
1>接口设计
1.1>接口说明:
类目
说明
请求方法
POST
url定义
/admin/group/
参数格式
表单参数
1.2>参数说明:
参数名
类型
是否必须
描述
name
字符串
是
分组名称
permissions
整数
否
权限id
1.3>返回数据
# 修改正常返回json数据
{
"errno": "0",
"errmsg": "添加分组成功!"
}
如果有错误,返回html表单
2>后端代码
2.1>视图
# 在admin/views.py中的GroupAddView视图中添加post方法如下
class GroupAddView(View):
"""
添加分组视图
"""
def post(self, request):
form = GroupModeForm(request.POST)
if form.is_valid():
form.save()
return json_response(errmsg='添加分组成功!')
else:
menus = models.Menu.objects.only('name', 'permission_id').select_related('permission').filter(
is_delete=False,
parent=None)
return render(request, 'admin/group/group_detail.html', context={'form': form, 'menus': menus})
3>前端代码
3.1>html
由于添加与修改分组使用的模板和js是同一个, 因此就要分辨到底是做POST还是PUT请求
<button type="button"
{% if form.instance.id %}
data-url="{% url 'myadmin:update_group' form.instance.id %}"
data-type="PUT"
{% else %}
data-url="{% url 'myadmin:add_group' %}"
data-type="POST"
{% endif %}
class="btn btn-primary pull-right save">保存
button>
在前端判断页面的地方添加一个data-type
, 其分别对应不同的请求方式(put/post), 即可实现代码的复用
3.2>js
// 修改 group_detail.js中保存按钮的js代码如下
// 保存按钮
$('.box-footer button.save').click(function () {
// 将表单中的数据进行格式化
$
.ajax({
url: $(this).data('url'),
// 可以拿到当前页面表单的数据,会自动拼接
data: $('form').serialize(),
type: $(this).data('type') // 前端的请求类型
})
.done((res) => {...})
.fail((res) => {...})
});
VI. 权限认证整合
小试牛刀
我们先来拿django内置的权限系统来试一下, 看看有哪些作用
由于我们后台的所有路由几乎都是使用的类视图的形式, 所有这次我们使用继承的方法实现, 官方文档
先拿菜单列表做一下试验:
# 在myadmin/views.py中修改一下代码
from django.contrib.auth.mixins import PermissionRequiredMixin
class MenuUpdateView(PermissionRequiredMixin, View):
# 权限名, 单个可以是字符串, 多个可以用元组
permission_required = 'myadmin.menu_update'
...
- 打开我们的项目页面
- 添加一个
修改菜单
权限, 权限码使用menu_update
- 给一个用户的分组添加上
菜单管理
权限, 但不添加修改菜单
权限
- 登录我们的目标用户(testzh), 打开菜单管理页
- 然后点击
编辑
, 你应该会得到一个403 Forbidden的错误
官方提供的权限系统非常实用, 但也具有局限性, 不满足我们的需求
接下来我们就通过重写的方式, 来完成我们需要的功能: 当没有权限访问时, 返回一个提示界面
1>业务需求
根据django内置的权限模块功能,可以很好的进行权限认证。但是本项目大量使用ajax,在进行权限认证时会遇到麻烦。且本项目的url设计符合RESTFUL api,所以在使用内置权限认证时也会出现问题。因此本项目对权限认证做了二次开发。
2>权限认证Mixin
在myadmin/views.py
中添加这个类:
class MyPermissionRequiredMinxin(PermissionRequiredMixin):
def handle_no_permission(self):
"""
覆盖父类方法,解决ajax返回json数据的问题
:return:
"""
if self.request.is_ajax():
if self.request.user.is_authenticated:
return json_response(errno=Code.ROLEERR, errmsg='您没有权限!' )
else:
return json_response(errno=Code.SESSIONERR, errmsg='您未登录,请登录!', data={'url': reverse(self.get_login_url())})
else:
return super().handle_no_permission()
def has_permission(self):
"""
覆盖父类方法,解决一个视图类有多个权限对象的问题
"""
perms = self.get_permission_required()
if isinstance(perms, dict):
if self.request.method.lower() in perms:
return self.request.user.has_perms(perms[self.request.method.lower()])
else:
return self.request.user.has_perms(perms)
注意在settings中设置LOGIN_URL
# 登录url
LOGIN_URL = 'user:login'
3>视图权限认证
使用方法和django提供的权限认证方法一致,新增同一个视图通过请求方式进行权限验证的功能。
#
class MenuUpdateView(MyPermissionRequiredMinxin, View):
"""
菜单管理视图
url:/admin/menu//
"""
# 不同请求,对应不同的权限
permission_required = {
'get': ('myadmin.menu_update',),
'put': ('myadmin.menu_update',),
'delete': ('myadmin.menu_delete',),
}
...
4>ajax接收处理
// 编辑菜单
$editBtns.click(function () {
let $this = $(this);
$currentMenu = $this.parent().parent();
menuId = $this.parent().data('id');
$
.ajax({
url: '/admin/menu/' + menuId + '/',
type: 'get'
})
.done((res) => {
if (res.errno === '4101') {
message.showError(res.errmsg);
setTimeout(() => {
window.location.href = res.data.url
}, 1500)
} else if (res.errno === '4105') {
message.showError(res.errmsg)
} else {
$('#modal-update .modal-content').html(res);
$('#modal-update').modal('show')
}
})
.fail(() => {
message.showError('服务器超时,请重试!')
})
});
在咱们的后台上创建响应的菜单即可, 记得在创建时尽量不要用权限表中重复的字段, 具体请参考数据库的auth_permissions
表格
权限分组告一段落, 之后咱么来搞一搞新闻管理页