Django组建策划之可插拔权限组件(中):RBAC

组件实现效果图

组件实现.gif

组件项目源代码

Django组建策划之可插拔权限组件RBAC(上)

本期涉及知识点

知识点一:自定义Tag(templatetags)inclusion_tag
知识点二:有序字典(OrderedDict())
知识点三:利用URL,get请求动态加载页面
知识点四:生成带有原搜索条件的URL(替代了模板中的url)QueryDict
知识点五,根据model字符类型CharField/TextField生成Radio单选框
知识点六:form表单之自动填充字段初始值
知识点七:form表单save前,保存前端表单未提交的数据
知识点八:批量生成formset
知识点九:批量修改字段的修改信息
知识点十:获取整个项目所有的URL
知识点十一:form渲染数据的常用格式
知识点十二:保存数据遇到的坑
知识点十三:保存多对多数据ORM操作
知识点十四:批量保存数据
知识点十五:获取前端多个表单form的数据,如何在后端区分是哪个表单进行提交

知识点一:自定义Tag(templatetags)inclusion_tag

在app下创建templatetags/app名.py
使用register.inclusion_tag()装饰器,使该Tag返回一个自定义html页面

In templatetags/appname.py
from django.tempalte import Library
register = Library()

@register.inclusion_tag('html页面路径')
def func_name(request):
    return data

In html页面
编写自定义的html页面,可根据函数return的数据,使用模板语法进行html页面的渲染

在layout_plus.html页面中{% load app名称 %},在需要自定义位置上{% 函数名 request %},记得要传入参数request,方便在tag函数中获取请求相关的数据

知识点二:有序字典(OrderedDict())

我们知道字典都是无序的,所以通过模板语法循环字典的话会导致数据顺序每次刷新页面都会不同,导致用户体验不佳。那么我们怎么让字典变成和列表元组等有序的呢?

from collections import OrderedDict  # 导入有序字典

menu_dict = request.session[settings.MENU_SESSION_KEY]  # 拿到初始化的菜单字典,如果直接用该字典生成左侧的菜单列表,则会出现排序混乱

# 因为menu_dict字典key为数字1/2/3,所以字典的key进行排序
key_list = sorted(menu_dict)  # sorted()默认为正序(升序)排序

# 空的有序字典
ordered_dict = OrderedDict()

for key in key_list:
    val = menu_dict[key]
    val['class'] = ''  # 默认每一个1级菜单不展开,隐藏二级菜单

    for per in val['children']:  # 循环二级菜单
        if per['id'] == request.current_selected_permission:  # 二级菜单ID = 当前选中菜单对应的ID/PID,用于判断展开哪个一级菜单
            per['class'] = 'active'
            val['class'] = 'nav-active'  # 让1级菜单的class由hide--->空
    ordered_dict[key] = val  # 给有序字典赋值


return {'menu_dict': ordered_dict}

OrderDict格式

ordered OrderedDict([
  ('1',
   {
    'title': '权限管理',
    'icon': 'fa-gears',
    'children': [
      {
        'id': 13,
        'title': '角色列表',
        'url': '/rbac/role/list/'
      },
      {
        'id': 17,
        'title': '菜单列表',
        'url': '/rbac/menu/list/',
        'class': 'active'
      },
      {
        'id': 29,
        'title': '权限分配',
        'url': '/rbac/distribute/permissions/'
      }],
    'class': 'nav-active'
  }),
  ('2',
   {
    'title': '用户管理',
    'icon': 'fa-users',
    'children': [
      {
        'id': 4,
        'title': '用户列表',
        'url': '/user/list/'
      }],
    'class': ''
  }),
  .........
])

知识点三:利用URL,get请求动态加载页面

html的a标签href属性,默认跳转当前页面,在href后添加?参数=xxx可实现GET访问带参数的URL页面
{{ row.title }}
用户点击后重新走一遍访问流程:WSGI--->中间键--->URL--->对应的视图函数
其中在视图函数中通过request.GET.get("?后的参数")获取参数对应的value,再根据value处理将在前端展示的数据--->template

知识点四:生成带有原搜索条件的URL(替代了模板中的url)QueryDict

如果我们要点击链接跳转到另外一个页面上,但是我们想保留当前URL上?后面的参数,让我们原本的搜索条件得到保存,则我们需要引入QueryDict

  1. 修改需要跳转的a标签上为SimpleTag
    使用方法和inclusion_tag类似,只是simpletag生成的是字符串

  2. 根据a标签href跳转的模板语法,参数用空格隔开,传给simpletag,使用QueryDict对url进行拼接

from django.urls import reverse
from django.http import QueryDict

def memory_url(request, name, *args, **kwargs):
    """
    生成带有原搜索条件的URL(替代了模板中的url)
    :param name: 反向生成URL的name
    """
    basic_url = reverse(name, args=args, kwargs=kwargs)

    # 当前URL中无参数
    if not request.GET:
        return basic_url

    query_dict = QueryDict(mutable=True)  # 生成QueryDict对象,mutable=True使对象可修改
    query_dict['_filter'] = request.GET.urlencode()  # mid=2&age=99

    return "%s?%s" % (basic_url, query_dict.urlencode())  # 返回拼接好的URL字符串
  1. 通过POST请求在视图函数中反向生成原有搜索条件的URL
In view.py
def menu_edit(request, pk):
    """
    编辑一级菜单
    :param request:
    :param pk:
    :return:
    """
    obj = models.Menu.objects.filter(id=pk).first()
    if not obj:
        return HttpResponse('菜单不存在')
    if request.method == 'GET':
        form = MenuModelForm(instance=obj)
        return render(request, 'rbac/menu_change.html', {'form': form})

    form = MenuModelForm(instance=obj, data=request.POST)
    if form.is_valid():
        form.save()
        return redirect(memory_reverse(request, 'rbac:menu_list'))  # 调用memory_reverse反向生成带有当前搜索条件的url

    return render(request, 'rbac/menu_change.html', {'form': form})
def memory_reverse(request, name, *args, **kwargs):
    """
    反向生成URL
        http://127.0.0.1:8001/rbac/menu/add/?_filter=mid%3D2
        1. 在url中讲原来搜索条件,如filter后的值
        2. reverse生成原来的URL,如:/menu/list/
        3. /menu/list/?mid%3D2
    """
    url = reverse(name, args=args, kwargs=kwargs)
    origin_params = request.GET.get('_filter')  # 获取当前页面url搜索条件_filter
    if origin_params:
        url = "%s?%s" % (url, origin_params,)  # 拼接url
    return url

知识点五,根据model字符类型CharField/TextField生成Radio单选框

models.py
class Menu(models.Model):
    """
    菜单表
    """
    title = models.CharField(verbose_name='菜单名称', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32)

form.py
ICON_LIST_PLUS = [['fa-bluetooth', 'fa fa-bluetooth'],
                  ['fa-bluetooth-b', 'fa fa-bluetooth-b'],
                  ......]]

class MenuModelForm(forms.ModelForm):
    class Meta:
        model = models.Menu
        fields = ['title', 'icon']
        widgets = {
            'title': forms.TextInput(attrs={'placeholder': '请输入菜单名称', 'class': 'form-control'}),
            'icon': forms.RadioSelect(
                choices=ICON_LIST_PLUS,
            )
        }
template
{% for foo in form.icon %}
{{ foo }}
{% endfor %}

知识点六:form表单之自动填充字段初始值

通过GET请求访问表单URL,如果我们需要给表单设置初始值,则普遍有两种方法:

  1. 给对象form表单全部字段填充数据初始值
    通常url会获取该对象的id,视图函数则可以通过id来过滤出该对象,例如:
permission_object = models.Permission.objects.filter(id=pk).first()

if request.method == 'GET':
    form = SecondMenuModelForm(instance=permission_object)  # Form(instance = obj)
    return render(request, 'rbac/change.html', {'form': form})

通过给Form添加instance=obj给Form表单全部字段填充初始值

  1. 给对象form表单指定字段填充数据初始值
    在视图函数中通过在前端传来的参数,获取到经过过滤条件的对象
def second_menu_add(request, menu_id):
    """
    添加二级菜单
    :param request:
    :param menu_id: 已选择的一级菜单ID(用于设置初始值)
    :return:
    """

    menu_object = models.Menu.objects.filter(id=menu_id).first()

    if request.method == 'GET':
        form = SecondMenuModelForm(initial={'menu': menu_object})  # initial={'字段':字段属性} 用于给该字段自动填充默认值
        return render(request, 'rbac/change.html', {'form': form})

通过给Form添加initial={form字段:form字段对象}给Form表单填充该字段的初始值,如果有多个字段需要初始值initial=[{字段:},{},{},.....

知识点七:form表单save前,保存前端表单未提交的数据

在特殊场景下,有些form字段不会让前端表单进行填写,此时在保存form表单数据时,会用到

if form.is_valid():
        # second_menu_id是通过url传递的参数
        second_menu_object = models.Permission.objects.filter(id=second_menu_id).first()
        # form.instance包含用户提交的所有值
        # PermissionModelForm添加了title,name,url,还需要添加pid则可以使用form.instance.pid = obj为pid添加对应值
    form.instance.字段名 = 字段对象
    form.save()

知识点八:批量生成Formset(表单集)是多个表单的集合

Formset在Web开发中应用很普遍,它可以让用户在同一个页面上提交多张表单,一键添加多个数据,比如一个页面上添加多个用户信息
导入from django.forms import formset_factory
语法formset_factory(form表单, extra/max_num)

generate_formset_class = formset_factory(MultiAddPermissionForm, extra=0)
update_formset_class = formset_factory(MultiEditPermissionForm, extra=0)
# extra: 想要显示表单form的数量,默认extra=1,会自动添加一行空表单
# max_num: 表单显示最大数量,可选,默认1000
注意: max_num优先级高于extra

在视图文件views.py和template里,我们可以像使用form一样使用formset
注意:前端使用formset必须引入 formset名.management_form ,例如:
{{ generate_formset.management_form }}
如果同一页面有多个formset,我们需要对其进行区分,可以根据URL设置参数?type的不同用于区别同一个页面不同的post请求,例如:


这样给URL设置跳转type后,当提交表单内容post请求,后端就需要使用request.GET.get("type")来做表单的区分,例如:

post_type = request.GET.get('type')  # 用于区别前端不同的post命令
if request.method == 'POST' and post_type == 'generate':
    pass
if request.method == 'POST' and post_type == 'update':
    pass

使用formset提交数据时会发现,formset如果不输入任何一个input框,则不会报出为空的错误,且如果只是输入一个input框,提交数据后,只报错这一行的form表单。那么我们怎样才能让每一个错误信息在当前的input框上呢?
在后端通过formset = update_formset_class(data=request.POST) 获取表单数据,使用formset.is_valid()检验数据对错。这时是一个[form,form,form,.....]的模型。我们需要循环formset拿到单个form进行校验,通过

if formset.is_valid():
    post_row_list = formset.cleaned_data
    for i in range(0,formset.total_form_count()):
        row_dict = post_row_list[i]

拿到每一个form的cleaned_data

如果我们要通过formset更新数据,必须在生成formset时的form模型中设置

    # formset修改页面必须添加id字段
    id = forms.IntegerField(
        widget=forms.HiddenInput()  # 隐藏字段HiddenInput(),为了formset能找到是哪一个id的数据进行了修改
    )

设置了之后formset会在前端会给ID生成一个隐藏的input框。当修改数据时,将会连同这个隐藏的value=id一起提交给后台。后台根据permission_id = row_dict.pop('id') 把前端隐藏的input的id拿到,用于对该id进行修改,使用pop是为了不修改数据id只修改表单内容

知识点九:批量修改字段的修改信息

当我们需要修改某对象的字段时,通常使用update.(**dict)
而当我们需要验证如知识点十二一样的异常处理时,需要用到save来保存数据。那怎么才能批量修改字段对应的信息呢?
普通青年版

obj.title = dict["title"]
obj.name = dict["name"]
obj.depart = dict["depart"]
....
obj.save()

由此可见需要重复书写一堆重复相似的代码
牛逼青年版

for key, value in dict.item():
    setattr(obj, key, value)  # 利用反射setattr(对象,key,value)为对象的key设置对应的value
obj.save()

知识点十:获取整个项目所有的URL

通过import_string获取根路由urlpatterns,循环urlpatterns拿到每个路由,进行判断 isinstance(url,RegexURLPattern/RegexURLResolve)路由是否能够继续分发。如果路由能继续分发,则继续递归,直到不能分发为止。如果全部路由都不能分发了,这说明路由已全部找到。把不能分发的路由添加到有序字典OrderDict中,用于前端展示路由系统

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from collections import OrderedDict
from django.conf import settings
from django.utils.module_loading import import_string

# Django1.*只需导入以下模块
from django.urls import RegexURLResolver, RegexURLPattern  # Django1.*  urlpatterns类型


# Django2.* 需要导入以下模块
# from django.urls import URLResolver, URLPattern  # Django2.* urlpatterns类型
# from pro_crm import urls  # Django2.* 获取项目全部URL,需要获取根urlpatterns


def check_url_exclude(url):
    """
    自定制排除一些特定的URL
    :param url:
    :return:
    """
    for regex in settings.AUTO_DISCOVER_EXCLUDE:
        if re.match(regex, url):  # 如果在白名单内的URL,则不需要进行全局URL展示
            return True


def recursion_urls(pre_namespace, pre_url, urlpatterns, url_ordered_dict):
    """
    递归的去获取URL
    :param pre_namespace: namespace前缀,以后用户拼接name
    :param pre_url: url前缀,以后用于拼接url
    :param urlpatterns: 路由关系列表
    :param url_ordered_dict: 用于保存递归中获取的所有路由
    :return:
    """
    for item in urlpatterns:
        if isinstance(item, RegexURLPattern):  # 非路由分发,将路由添加到url_ordered_dict
            if not item.name:  # 如果没有name则无法添加到全局URL内
                continue

            if pre_namespace:
                name = "%s:%s" % (pre_namespace, item.name)
            else:
                name = item.name

            ######### Django1.* 获取regex路径方法 item._regex获取当前URL前缀 #########
            url = pre_url + item._regex  # /^rbac/^user/edit/(?P\d+)$/

            ######### Django2.* 获取regex路径方法 item.pattern.regex.pattern#########
            # url = pre_url + item.pattern.regex.pattern  # /rbac/user/edit/(?P\d+)/

            url = url.replace('^', '').replace('$', '')  # 去除正则匹配符号

            if check_url_exclude(url):  # 如果是白名单的路由,则不需要匹配出来
                continue

            url_ordered_dict[name] = {'name': name, 'url': url}  # 添加进有序字典中

        elif isinstance(item, RegexURLResolver):  # 路由分发,递归操作

            # 获取namespace
            if pre_namespace:  # include的URL有namespace
                if item.namespace:  # 分发后的路由有namespace
                    namespace = "%s:%s" % (pre_namespace, item.namespace,)
                else:
                    namespace = item.namespace
            else:  # include的URL没有namespace
                if item.namespace:  # 分发后的路由有namespace,而父URL没有namespace
                    namespace = item.namespace
                else:
                    namespace = None
            ######### Django1.* 有两种获取获取正则路径的方式#########
            # 递归进入更深层的include路由,第二个参数前缀的URL需要把上一层的URL和这一层的URL加起来
            # 方法一:
            recursion_urls(namespace, pre_url + item.regex.pattern, item.url_patterns, url_ordered_dict)
            # 方法二:
            # recursion_urls(namespace, pre_url + item._regex, item.url_patterns, url_ordered_dict)

            ######### Django2.* #########
            # recursion_urls(namespace, pre_url + item.pattern.regex.pattern, item.url_patterns, url_ordered_dict)


def get_all_url_dict():
    """
    获取项目中所有的URL(必须有name别名)
    :return:
    """
    url_ordered_dict = OrderedDict()  # 创建有序字典

    ##### Django1.*使用import_string获取urlpatterns #####
    md = import_string(settings.ROOT_URLCONF)  # from luff.. import urls  获取到项目根路由ROOT_URLCONF
    recursion_urls(None, '/', md.urlpatterns, url_ordered_dict)  # 递归去获取所有的路由,默认路由前缀为/

    ##### Django2.*需导入urlpatterns获取 #####
    # print(urls.urlpatterns)
    # recursion_urls(None, '/', urls.urlpatterns, url_ordered_dict)  # 递归去获取所有的路由

    return url_ordered_dict

知识点十一:form渲染数据的常用格式

我们一般使用form渲染前端页面无非两种:

  1. form表单input框label等标签
    直接在后端拿到queryset/form序列化等数据格式,使用模板语法在前端进行for循环,自动生成前端标签
  2. table标签th、td和有层次的ul/li等多维标签
    通常需要先指定th,再用模板语法for循环,生成对应的td标签,td标签为了与th一一对应,通常使用循环的对象.字段属性
    如果不想在前端手写th标签内容,可以在前端生成双层嵌套模型(字典套字典/列表套字典/列表套对象.....),让前端模板语法双层循环后端内容

知识点十二:保存数据遇到的坑

保存一个数据时,如果没有在保存前通过表单校验某字段必须唯一,而直接保存数据models.Permission.objects.create(**dict) 。其中使用create保存数据时,会忽略错误信息UNIQUE唯一索引,而导致报错,为了避免这个错误,建议使用

try:
    obj = models.Permission(**dict)
    obj.validate_unique()# 检查当前对象数据库是否存在唯一索引的异常.validate_unique()
    obj.save
except Exception as e:
    pass

知识点十三:保存多对多数据ORM操作

保存多对多数据一般有两种操作:
1. add()
 通常情况使用add(),拿到前端某个过滤条件,拿到过滤后的对象使用add与多对多关系的表进行关联
语法:子表对象.子表多对多字段.add( QuerySet对象 xxx.objects.all() )
2. set()
 set里传入列表([1,5,9,12,15....]),列表内元素为多对多数据的ID,通常set用于前端的checkbox传过来的value
语法*:字表对象.多对多字段.set(多对多字段的id列表)

知识点十四:批量保存数据

在有大量数据进行添加时,例如批量添加。把前端拿来的数据在后端进行验证,循环获取每一个通过验证的对象,如果每次循环进行一次数据库的保存操作,则会大量消耗资源。所以我们通常把对象添加到列表内,再使用bulk_create(列表,,batch_size=100)进行批量添加,例如:
models.Permission.objects.bulk_create(object_list, batch_size=100) # 批量增加数据

知识点十五:获取前端多个表单form的数据,如何在后端区分是哪个表单进行提交

在我们单个页面有多个form表单时,我们需要对每一个form表单进行标识,让每一个提交到后台的数据有所区分。
方法一:给form标签action设置参数
前端:
{# 根据URLtype的不同用于区别同一个页面不同的post请求 #}
后端:

post_type = request.GET.get('type')  # 用于区别前端不同的post命令
if request.method == 'POST' and post_type == 'generate':
    pass
if request.method == 'POST' and post_type == 'update':
    pass

方法二:给前端添加隐藏input框,从而通过form获取input自定义的name:value
前端:

   设置一个隐藏的input,当form获取表单内容时,传入name:value=type:role用于后端区分

后端:

if request.method == 'POST' and request.POST.get('type') == 'role':  # 由前端隐藏字段type:role进行form表单的区分
    pass
if request.method == 'POST' and request.POST.get('type') == 'permission':
    pass

注意!:使用方法二,会导致出现一个问题,如果前端有人恶意修改input标签,导致跳过某些条件也能实现提交,则需要自行在后端进行排查

Django组建策划之可插拔权限组件RBAC(下)

你可能感兴趣的:(Django组建策划之可插拔权限组件(中):RBAC)