From 509d14e72355cc998aa829720f3182e57ec03088 Mon Sep 17 00:00:00 2001 From: Angelo Date: Fri, 7 Jul 2023 19:04:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20LazyLoadFilter=E6=87=92=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E8=BF=87=E6=BB=A4=E5=99=A8=E6=89=A9=E5=B1=95=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/dvadmin/system/views/dept.py | 11 ++- backend/dvadmin/utils/filters.py | 107 ++++++++++++++++++--------- web/src/views/system/dept/crud.js | 8 +- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/backend/dvadmin/system/views/dept.py b/backend/dvadmin/system/views/dept.py index 7b5a6a1..da9bb61 100644 --- a/backend/dvadmin/system/views/dept.py +++ b/backend/dvadmin/system/views/dept.py @@ -13,6 +13,7 @@ from dvadmin.utils.json_response import DetailResponse, SuccessResponse from dvadmin.utils.permission import AnonymousUserPermission from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet +from dvadmin.utils.filters import LazyLoadFilter class DeptSerializer(CustomModelSerializer): @@ -120,6 +121,12 @@ class DeptCreateUpdateSerializer(CustomModelSerializer): fields = '__all__' +class DeptLazyFilter(LazyLoadFilter): + class Meta: + model = Dept + fields = ['name', 'parent', 'status'] + + class DeptViewSet(CustomModelViewSet): """ 部门管理接口 @@ -129,11 +136,13 @@ class DeptViewSet(CustomModelViewSet): retrieve:单例 destroy:删除 """ + queryset = Dept.objects.all() serializer_class = DeptSerializer create_serializer_class = DeptCreateUpdateSerializer update_serializer_class = DeptCreateUpdateSerializer - filter_fields = ['name', 'id', 'parent'] + # filter_fields = ["name", "id", "parent"] + filter_class = DeptLazyFilter search_fields = [] # extra_filter_backends = [] import_serializer_class = DeptImportSerializer diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index ebb4cb5..24d6d4c 100644 --- a/backend/dvadmin/utils/filters.py +++ b/backend/dvadmin/utils/filters.py @@ -12,6 +12,7 @@ from collections import OrderedDict from functools import reduce import six +from django import forms from django.db import models from django.db.models import Q, F from django.db.models.constants import LOOKUP_SEP @@ -19,6 +20,7 @@ from django_filters import utils from django_filters.conf import settings from django_filters.constants import ALL_FIELDS from django_filters.filters import CharFilter +from django_filters.filterset import FilterSet, FilterSetMetaclass from django_filters.rest_framework import DjangoFilterBackend from django_filters.utils import get_model_field from rest_framework.filters import BaseFilterBackend @@ -71,9 +73,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend): permission__api=F("url"), permission__method=F("method") ) api_white_list = [ - str(item.get("permission__api").replace("{id}", ".*?")) - + ":" - + str(item.get("permission__method")) + str(item.get("permission__api").replace("{id}", ".*?")) + ":" + str(item.get("permission__method")) for item in api_white_list if item.get("permission__api") ] @@ -119,19 +119,13 @@ class DataLevelPermissionsFilter(BaseFilterBackend): # 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据) if 0 in dataScope_list: - return queryset.filter( - creator=request.user, dept_belong_id=user_dept_id - ) + return queryset.filter(creator=request.user, dept_belong_id=user_dept_id) # 5. 自定数据权限 获取部门,根据部门过滤 dept_list = [] for ele in dataScope_list: if ele == 4: - dept_list.extend( - request.user.role.filter(status=1).values_list( - "dept__id", flat=True - ) - ) + dept_list.extend(request.user.role.filter(status=1).values_list("dept__id", flat=True)) elif ele == 2: dept_list.append(user_dept_id) elif ele == 1: @@ -141,7 +135,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend): user_dept_id, ) ) - if queryset.model._meta.model_name == 'dept': + if queryset.model._meta.model_name == "dept": return queryset.filter(id__in=list(set(dept_list))) return queryset.filter(dept_belong_id__in=list(set(dept_list))) else: @@ -186,16 +180,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): # TODO: remove assertion in 2.1 if filterset_class is None and hasattr(view, "filter_class"): utils.deprecate( - "`%s.filter_class` attribute should be renamed `filterset_class`." - % view.__class__.__name__ + "`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__ ) filterset_class = getattr(view, "filter_class", None) # TODO: remove assertion in 2.1 if filterset_fields is None and hasattr(view, "filter_fields"): utils.deprecate( - "`%s.filter_fields` attribute should be renamed `filterset_fields`." - % view.__class__.__name__ + "`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__ ) filterset_fields = getattr(view, "filter_fields", None) @@ -224,8 +216,9 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): return [ f.name for f in sorted(opts.fields + opts.many_to_many) - if (f.name == 'id') or not isinstance(f, models.AutoField) - and not (getattr(f.remote_field, "parent_link", False)) + if (f.name == "id") + or not isinstance(f, models.AutoField) + and not (getattr(f.remote_field, "parent_link", False)) ] @classmethod @@ -239,9 +232,9 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): exclude = cls._meta.exclude assert not (fields is None and exclude is None), ( - "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " - "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " - "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__ + "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " + "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " + "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__ ) # Setting exclude with no fields implies all other fields. @@ -255,9 +248,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): # Remove excluded fields exclude = exclude or [] if not isinstance(fields, dict): - fields = [ - (f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude - ] + fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude] else: fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] @@ -291,9 +282,12 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): if field is None: undefined.append(field_name) # 更新默认字符串搜索为模糊搜索 - if isinstance(field, (models.CharField)) and filterset_fields == '__all__' and lookups == [ - 'exact']: - lookups = ['icontains'] + if ( + isinstance(field, (models.CharField)) + and filterset_fields == "__all__" + and lookups == ["exact"] + ): + lookups = ["icontains"] for lookup_expr in lookups: filter_name = cls.get_filter_name(field_name, lookup_expr) @@ -303,20 +297,15 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): continue if field is not None: - filters[filter_name] = cls.filter_for_field( - field, field_name, lookup_expr - ) + filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr) # Allow Meta.fields to contain declared filters *only* when a list/tuple if isinstance(cls._meta.fields, (list, tuple)): - undefined = [ - f for f in undefined if f not in cls.declared_filters - ] + undefined = [f for f in undefined if f not in cls.declared_filters] if undefined: raise TypeError( - "'Meta.fields' must not contain non-model field names: %s" - % ", ".join(undefined) + "'Meta.fields' must not contain non-model field names: %s" % ", ".join(undefined) ) # Add in declared filters. This is necessary since we don't enforce adding @@ -364,3 +353,51 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): if not filterset.is_valid() and self.raise_exception: raise utils.translate_validation(filterset.errors) return filterset.qs + + +# ####################### 懒加载FilterSet ####################### # + + +class FilterSetOptions: + def __init__(self, options=None): + self.model = getattr(options, "model", None) + self.fields = getattr(options, "fields", None) + self.exclude = getattr(options, "exclude", None) + + # CharField默认模糊查询 + self.filter_overrides = getattr( + options, + "filter_overrides", + { + models.CharField: { + "filter_class": CharFilter, + "extra": lambda f: { + "lookup_expr": "icontains", + }, + } + }, + ) + + self.form = getattr(options, "form", forms.Form) + + +class LazyLoadFilterSetMetaclass(FilterSetMetaclass): + def __new__(cls, name, bases, attrs): + attrs["declared_filters"] = cls.get_declared_filters(bases, attrs) + + new_class = super().__new__(cls, name, bases, attrs) + new_class._meta = FilterSetOptions(getattr(new_class, "Meta", None)) + new_class.base_filters = new_class.get_filters() + + return new_class + + +class LazyLoadFilter(FilterSet, metaclass=LazyLoadFilterSetMetaclass): + @property + def qs(self): + is_node = self.queryset.filter(parent__isnull=False).exists() + if not is_node: + self.queryset = self.queryset.model.objects.filter(parent__in=self.queryset) + parent_ids = set(super().qs.values_list("parent_id", flat=True)) + return self.queryset.model.objects.filter(id__in=parent_ids) + return super().qs diff --git a/web/src/views/system/dept/crud.js b/web/src/views/system/dept/crud.js index ab6311f..2fe3247 100644 --- a/web/src/views/system/dept/crud.js +++ b/web/src/views/system/dept/crud.js @@ -20,7 +20,13 @@ export const crudOptions = (vm) => { hasChild: 'hasChild', lazy: true, loadMethod: ({ row }) => { - return api.GetList({ parent: row.id }).then(ret => { + let query = JSON.parse(JSON.stringify(vm.getSearch().getForm())) + query = Object.fromEntries( + Object.entries(query).filter(([_, value]) => ![undefined, null, [], '[]', ''].includes(value)) + ) + query.parent = row.id + // console.log(query) + return api.GetList({ ...query }).then(ret => { return ret.data.data }) },