django笔记|[django项目] 后台权限分组管理

权限分组管理 将多种权限合并到一个组中, 分配时即可一次性分配多种权限, 例如, 售后具有某几种权限, 当有成员被配置为他时就会拥有着几种权限
I. 权限分组列表 首先咱们来实现权限分组列表, 目的是展示所有的权限组, 同样需要添加一些权限组
1>接口设计

  1. 接口说明
类目 说明
请求方法 GET
url定义 /admin/groups/
参数格式 无参数
  1. 返回结果
    html
2>后端代码
2.1>视图
# 在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})

2.2>路由
# 在myadmin/urls.py中添加如下路由 path('groups/', views.GroupsView.as_view(), name='group_list')

3>前端代码
3.1>html 可以先写一个简单的模板, 然后运行到前端看一下
{% extends 'myadmin/base/content_base.html' %} {% load static %} {% load news_template_filters %}{# 分页过滤器 #} {% block page_header %} 系统设置 {% endblock %} {% block page_option %} 分组管理 {% endblock %}

首先来到菜单管理创建一个名字为分组管理的菜单路由地址是group_list
然后刷新页面即可看到我们的分组管理页面
django笔记|[django项目] 后台权限分组管理
文章图片

接下来我们来填充它的内容, 修改group_list.html模板
{% extends 'admin/content_base.html' %} {% load static %} {% load news_template_filters %} {# 分页过滤器 #} {% block page_header %} 系统设置 {% endblock %} {% block page_option %} 权限分组 {% endblock %} {% block content %}
分组列表
{% for group in groups %} {% endfor %}
# 组名 菜单
{{ forloop.counter }} {{ group.name }} {% for permis in group.permissions.all %} {{ permis.name }}/ {% empty %} 暂未分配权限 {% endfor %}
{% endblock %}

4>添加数据
前端页面写好了, 但是我们的权限分组仍然只是一个空列表, 我们可以先使用sql语句添加一些数据, 为后续的详情页做铺垫
【django笔记|[django项目] 后台权限分组管理】运行添加分组的语句(例)
INSERT INTO auth_groups(name) VALUES('售后');

运行添加分组权限的语句(例)
INSERT INTO auth_group_permissions(group_id, permission_id) VALUES(1, 70);

对应的权限码你可以在auth_permission表中看到, 选择我们在菜单管理中创建的
添加完成后的效果
django笔记|[django项目] 后台权限分组管理
文章图片

II. 权限分组详情页 1>接口设计
  1. 接口说明:
类目 说明
请求方法 GET
url定义 /admin/group//
参数格式 路径参数
  1. 参数说明:
参数名 类型 是否必须 描述
group_id 整数 分组id
  1. 返回数据
    返回html表单
2>后端代码
2.1>视图
# 在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 })

2.2>路由
# 在admin/urls.py中添加如下路由 path('group//', views.GroupUdateView.as_view(), name='update_group')

2.3>表单
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']

3>前端代码
3.1>html
{% extends 'myadmin/base/content_base.html' %} {% load static %} {% load admin_customer_tags %} {% block page_header %} 系统设置 {% endblock %} {% block page_option %} 权限分组 {% endblock %}{% block content %}
分组详情
{% csrf_token %}{% for field in form %} {% if field.name == 'permissions' %}
{% for error in field.errors %} {% endfor %} {% for menu in menus %}
{% for child in menu.children.all %}
{% endfor %}
{% endfor %}
{% else %}
{% for error in field.errors %} {% endfor %} {% add_class field 'form-control' %}
{% endif %} {% endfor %}
{% endblock %}

记得修改一下group_list.html中的data_url属性, 他的目的是让我们可以点击编号跳转到权限详情页
{{ forloop.counter }}

3.2>js
// 创建 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 %} src="https://www.it610.com/article/{% static'js/myadmin/group/group_list.js'%}"> {% endblock %}

III. 权限分组修改 1>接口设计
  1. 接口说明:
类目 说明
请求方法 PUT
url定义 /admin/group//
参数格式 路径参数+表单参数
  1. 参数说明:
参数名 类型 是否必须 描述
group_id 整数 分组id
name 字符串 分组名称
permissions 整数 权限id
  1. 返回数据
    # 修改正常返回json数据 { "errno": "0", "errmsg": "用户修改成功!" }

    如果有错误,返回html表单
2>后端代码
2.1>视图
# 在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 = https://www.it610.com/article/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 })

3>前端代码
3.1>js
# 创建 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) } } }) }); });

Checkbox跨级联动
前端模板添加两个属性onetwo, 分别代表一级和二级菜单
{% 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

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请求

在前端判断页面的地方添加一个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 PermissionRequiredMixinclass MenuUpdateView(PermissionRequiredMixin, View): # 权限名, 单个可以是字符串, 多个可以用元组 permission_required = 'myadmin.menu_update' ...

  1. 打开我们的项目页面
  2. 添加一个修改菜单权限, 权限码使用menu_update
  3. 给一个用户的分组添加上菜单管理权限, 但不添加修改菜单权限
  4. 登录我们的目标用户(testzh), 打开菜单管理页
  5. 然后点击编辑, 你应该会得到一个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=https://www.it610.com/article/{'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: returnself.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 = https://www.it610.com/article/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表格
权限分组告一段落, 之后咱么来搞一搞新闻管理页

    推荐阅读