diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index 4141c57..6e9101a 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -131,6 +131,7 @@ class Dept(CoreModel): null=True, blank=True, help_text="上级部门", + db_index=True ) @classmethod 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/system/views/menu_button.py b/backend/dvadmin/system/views/menu_button.py index 16e0feb..61e64d8 100644 --- a/backend/dvadmin/system/views/menu_button.py +++ b/backend/dvadmin/system/views/menu_button.py @@ -23,7 +23,7 @@ class MenuButtonSerializer(CustomModelSerializer): class Meta: model = MenuButton - fields = ['id', 'name', 'value', 'api', 'method', 'menu'] + fields = ["id", "name", "value", "api", "method", "menu"] read_only_fields = ["id"] @@ -34,7 +34,7 @@ class MenuButtonInitSerializer(CustomModelSerializer): class Meta: model = MenuButton - fields = ['id', 'name', 'value', 'api', 'method', 'menu'] + fields = ["id", "name", "value", "api", "method", "menu"] read_only_fields = ["id"] @@ -58,21 +58,26 @@ class MenuButtonViewSet(CustomModelViewSet): retrieve:单例 destroy:删除 """ + queryset = MenuButton.objects.all() serializer_class = MenuButtonSerializer create_serializer_class = MenuButtonCreateUpdateSerializer update_serializer_class = MenuButtonCreateUpdateSerializer extra_filter_backends = [] - @action(methods=['get'], detail=False) - def get_btn_permission(self,request): + @action(methods=["GET"], detail=False, permission_classes=[]) + def get_btn_permission(self, request): """ 获取当前用户的按钮权限 """ user = request.user if not user.is_superuser: - menuIds = user.role.values_list('menu__id', flat=True) + menuIds = user.role.values_list("menu__id", flat=True) else: menuIds = Menu.objects.filter(status=1) - queryset = MenuButton.objects.filter(menu__in=menuIds).annotate(permission=Concat('menu__web_path',Value(':'),'value',output_field=CharField())).values_list('permission',flat=True) + queryset = ( + MenuButton.objects.filter(menu__in=menuIds) + .annotate(permission=Concat("menu__web_path", Value(":"), "value", output_field=CharField())) + .values_list("permission", flat=True) + ) return DetailResponse(data=queryset) diff --git a/backend/dvadmin/utils/exception.py b/backend/dvadmin/utils/exception.py index 37adebe..53549c0 100644 --- a/backend/dvadmin/utils/exception.py +++ b/backend/dvadmin/utils/exception.py @@ -11,7 +11,7 @@ import traceback from django.db.models import ProtectedError, RestrictedError from django.http import Http404 -from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed +from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, PermissionDenied from rest_framework.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED from rest_framework.views import set_rollback, exception_handler @@ -50,6 +50,8 @@ def CustomExceptionHandler(ex, context): elif isinstance(ex, DRFAPIException): set_rollback() msg = ex.detail + if isinstance(ex, PermissionDenied): + msg = f'{msg} ({context["request"].method}: {context["request"].path})' if isinstance(msg, dict): for k, v in msg.items(): for i in v: diff --git a/backend/dvadmin/utils/filters.py b/backend/dvadmin/utils/filters.py index ebb4cb5..52de84a 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,113 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): if not filterset.is_valid() and self.raise_exception: raise utils.translate_validation(filterset.errors) return filterset.qs + + +# ####################### 懒加载FilterSet ####################### # + +import time + + +def calculate_execution_time(func): + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + execution_time = end_time - start_time + print(f"Function {func.__name__} took {execution_time:.6f} seconds to execute.", flush=True) + return result + + return wrapper + + +# def get_children(model: models, obj_id: int, all_qs=None, rec_list=None): +# if not all_qs: +# all_qs = model.objects.all().values("id", "parent") +# if rec_list is None: +# rec_list = [obj_id] +# for ele in all_qs: +# if ele.get("parent") == obj_id: +# rec_list.append(ele.get("id")) +# get_dept(ele.get("id"), all_qs, rec_list) +# return list(set(rec_list)) + + +# @calculate_execution_time +# def get_qs_children(model, qs): +# dept_ids = [] +# for d in qs: +# r = get_children(model, d.id) +# dept_ids.extend(r) +# return list(set(dept_ids)) + + +def next_layer_data(qs_filter, qs_node): + parent_nodes = set(qs_node.values_list("id", flat=True)) + # print(f"过滤查询集 ==> {qs_filter}", flush=True) + # print(f"待渲染节点的id ==> {parent_nodes=}", flush=True) + if set(qs_filter) == set(qs_node): + return parent_nodes + # qs_filter内所有父级id 去重 + parent_ids = set() + for node in qs_filter: + while node.parent: + if node.id in parent_nodes: + parent_ids.add(node.id) + break + if node.parent.id in parent_nodes: + parent_ids.add(node.parent.id) + break + node = node.parent + # print(f"过滤查询集的父节点id ==> {parent_ids=}", flush=True) + return parent_ids + + +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): + # @calculate_execution_time + @property + def qs(self): + queryset = self.queryset + filter_params = [k for k, v in self.form.cleaned_data.items() if not v] + for field in filter_params: + self.form.cleaned_data.pop(field) + self.form.cleaned_data.pop("parent", None) + # print(queryset, flush=True) + if self.form.cleaned_data: + self.queryset = queryset.model.objects.all() + node_ids = next_layer_data(super().qs, queryset) + return queryset.model.objects.filter(id__in=node_ids) + return super().qs diff --git a/web/src/views/system/dept/crud.js b/web/src/views/system/dept/crud.js index ab6311f..a397850 100644 --- a/web/src/views/system/dept/crud.js +++ b/web/src/views/system/dept/crud.js @@ -13,6 +13,7 @@ export const crudOptions = (vm) => { height: '100%', // 表格高度100%, 使用toolbar必须设置 highlightCurrentRow: false, defaultExpandAll: true, + resizable: true, treeConfig: { transform: true, rowField: 'id', @@ -20,7 +21,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 }) }, @@ -28,6 +35,7 @@ export const crudOptions = (vm) => { } }, rowHandle: { + fixed: 'right', width: 140, view: { thin: true, @@ -55,7 +63,7 @@ export const crudOptions = (vm) => { // 或者直接传true,不显示title,不居中 title: '序号', align: 'center', - width: 100 + width: 70 }, viewOptions: { @@ -71,7 +79,7 @@ export const crudOptions = (vm) => { show: false, disabled: true, search: { - disabled: false + disabled: true }, form: { disabled: true, @@ -136,8 +144,8 @@ export const crudOptions = (vm) => { } } }, - width: 180, type: 'input', + showOverflow: 'tooltip', form: { rules: [ // 表单校验规则 diff --git a/web/src/views/system/messageCenter/crud.js b/web/src/views/system/messageCenter/crud.js index 115863c..94a56e7 100644 --- a/web/src/views/system/messageCenter/crud.js +++ b/web/src/views/system/messageCenter/crud.js @@ -275,7 +275,7 @@ export const crudOptions = (vm) => { return request({ url: url }).then(ret => { - return ret.data.data + return ret.data }) } },