[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项目] 后台权限分组管理_第1张图片

接下来我们来填充它的内容, 修改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 %}

4>添加数据

前端页面写好了, 但是我们的权限分组仍然只是一个空列表, 我们可以先使用sql语句添加一些数据, 为后续的详情页做铺垫

运行添加分组的语句(例)

INSERT INTO auth_groups(name) VALUES('售后');

运行添加分组权限的语句(例)

INSERT INTO auth_group_permissions(group_id, permission_id) VALUES(1, 70);

对应的权限码你可以在auth_permission表中看到, 选择我们在菜单管理中创建的

添加完成后的效果

[django项目] 后台权限分组管理_第2张图片

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

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 %}
    <script src="{% static 'js/myadmin/group/group_list.js'%}">script>
{% 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 = 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


<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'
    ...
  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={'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表格

权限分组告一段落, 之后咱么来搞一搞新闻管理页

你可能感兴趣的:(django笔记,django)