Merge remote-tracking branch 'origin/dev' into dev

pull/102/head
李强 2023-07-08 20:09:14 +08:00
commit 1180ab205e
7 changed files with 172 additions and 48 deletions

View File

@ -131,6 +131,7 @@ class Dept(CoreModel):
null=True, null=True,
blank=True, blank=True,
help_text="上级部门", help_text="上级部门",
db_index=True
) )
@classmethod @classmethod

View File

@ -13,6 +13,7 @@ from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.permission import AnonymousUserPermission from dvadmin.utils.permission import AnonymousUserPermission
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.filters import LazyLoadFilter
class DeptSerializer(CustomModelSerializer): class DeptSerializer(CustomModelSerializer):
@ -120,6 +121,12 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class DeptLazyFilter(LazyLoadFilter):
class Meta:
model = Dept
fields = ['name', 'parent', 'status']
class DeptViewSet(CustomModelViewSet): class DeptViewSet(CustomModelViewSet):
""" """
部门管理接口 部门管理接口
@ -129,11 +136,13 @@ class DeptViewSet(CustomModelViewSet):
retrieve:单例 retrieve:单例
destroy:删除 destroy:删除
""" """
queryset = Dept.objects.all() queryset = Dept.objects.all()
serializer_class = DeptSerializer serializer_class = DeptSerializer
create_serializer_class = DeptCreateUpdateSerializer create_serializer_class = DeptCreateUpdateSerializer
update_serializer_class = DeptCreateUpdateSerializer update_serializer_class = DeptCreateUpdateSerializer
filter_fields = ['name', 'id', 'parent'] # filter_fields = ["name", "id", "parent"]
filter_class = DeptLazyFilter
search_fields = [] search_fields = []
# extra_filter_backends = [] # extra_filter_backends = []
import_serializer_class = DeptImportSerializer import_serializer_class = DeptImportSerializer

View File

@ -23,7 +23,7 @@ class MenuButtonSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MenuButton model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu'] fields = ["id", "name", "value", "api", "method", "menu"]
read_only_fields = ["id"] read_only_fields = ["id"]
@ -34,7 +34,7 @@ class MenuButtonInitSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MenuButton model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu'] fields = ["id", "name", "value", "api", "method", "menu"]
read_only_fields = ["id"] read_only_fields = ["id"]
@ -58,21 +58,26 @@ class MenuButtonViewSet(CustomModelViewSet):
retrieve:单例 retrieve:单例
destroy:删除 destroy:删除
""" """
queryset = MenuButton.objects.all() queryset = MenuButton.objects.all()
serializer_class = MenuButtonSerializer serializer_class = MenuButtonSerializer
create_serializer_class = MenuButtonCreateUpdateSerializer create_serializer_class = MenuButtonCreateUpdateSerializer
update_serializer_class = MenuButtonCreateUpdateSerializer update_serializer_class = MenuButtonCreateUpdateSerializer
extra_filter_backends = [] extra_filter_backends = []
@action(methods=['get'], detail=False) @action(methods=["GET"], detail=False, permission_classes=[])
def get_btn_permission(self,request): def get_btn_permission(self, request):
""" """
获取当前用户的按钮权限 获取当前用户的按钮权限
""" """
user = request.user user = request.user
if not user.is_superuser: if not user.is_superuser:
menuIds = user.role.values_list('menu__id', flat=True) menuIds = user.role.values_list("menu__id", flat=True)
else: else:
menuIds = Menu.objects.filter(status=1) 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) return DetailResponse(data=queryset)

View File

@ -11,7 +11,7 @@ import traceback
from django.db.models import ProtectedError, RestrictedError from django.db.models import ProtectedError, RestrictedError
from django.http import Http404 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.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED
from rest_framework.views import set_rollback, exception_handler from rest_framework.views import set_rollback, exception_handler
@ -50,6 +50,8 @@ def CustomExceptionHandler(ex, context):
elif isinstance(ex, DRFAPIException): elif isinstance(ex, DRFAPIException):
set_rollback() set_rollback()
msg = ex.detail msg = ex.detail
if isinstance(ex, PermissionDenied):
msg = f'{msg} ({context["request"].method}: {context["request"].path})'
if isinstance(msg, dict): if isinstance(msg, dict):
for k, v in msg.items(): for k, v in msg.items():
for i in v: for i in v:

View File

@ -12,6 +12,7 @@ from collections import OrderedDict
from functools import reduce from functools import reduce
import six import six
from django import forms
from django.db import models from django.db import models
from django.db.models import Q, F from django.db.models import Q, F
from django.db.models.constants import LOOKUP_SEP 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.conf import settings
from django_filters.constants import ALL_FIELDS from django_filters.constants import ALL_FIELDS
from django_filters.filters import CharFilter from django_filters.filters import CharFilter
from django_filters.filterset import FilterSet, FilterSetMetaclass
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
@ -71,9 +73,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
permission__api=F("url"), permission__method=F("method") permission__api=F("url"), permission__method=F("method")
) )
api_white_list = [ api_white_list = [
str(item.get("permission__api").replace("{id}", ".*?")) str(item.get("permission__api").replace("{id}", ".*?")) + ":" + str(item.get("permission__method"))
+ ":"
+ str(item.get("permission__method"))
for item in api_white_list for item in api_white_list
if item.get("permission__api") if item.get("permission__api")
] ]
@ -119,19 +119,13 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据) # 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
if 0 in dataScope_list: if 0 in dataScope_list:
return queryset.filter( return queryset.filter(creator=request.user, dept_belong_id=user_dept_id)
creator=request.user, dept_belong_id=user_dept_id
)
# 5. 自定数据权限 获取部门,根据部门过滤 # 5. 自定数据权限 获取部门,根据部门过滤
dept_list = [] dept_list = []
for ele in dataScope_list: for ele in dataScope_list:
if ele == 4: if ele == 4:
dept_list.extend( dept_list.extend(request.user.role.filter(status=1).values_list("dept__id", flat=True))
request.user.role.filter(status=1).values_list(
"dept__id", flat=True
)
)
elif ele == 2: elif ele == 2:
dept_list.append(user_dept_id) dept_list.append(user_dept_id)
elif ele == 1: elif ele == 1:
@ -141,7 +135,7 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
user_dept_id, 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(id__in=list(set(dept_list)))
return queryset.filter(dept_belong_id__in=list(set(dept_list))) return queryset.filter(dept_belong_id__in=list(set(dept_list)))
else: else:
@ -186,16 +180,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# TODO: remove assertion in 2.1 # TODO: remove assertion in 2.1
if filterset_class is None and hasattr(view, "filter_class"): if filterset_class is None and hasattr(view, "filter_class"):
utils.deprecate( utils.deprecate(
"`%s.filter_class` attribute should be renamed `filterset_class`." "`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__
% view.__class__.__name__
) )
filterset_class = getattr(view, "filter_class", None) filterset_class = getattr(view, "filter_class", None)
# TODO: remove assertion in 2.1 # TODO: remove assertion in 2.1
if filterset_fields is None and hasattr(view, "filter_fields"): if filterset_fields is None and hasattr(view, "filter_fields"):
utils.deprecate( utils.deprecate(
"`%s.filter_fields` attribute should be renamed `filterset_fields`." "`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
% view.__class__.__name__
) )
filterset_fields = getattr(view, "filter_fields", None) filterset_fields = getattr(view, "filter_fields", None)
@ -224,8 +216,9 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
return [ return [
f.name f.name
for f in sorted(opts.fields + opts.many_to_many) for f in sorted(opts.fields + opts.many_to_many)
if (f.name == 'id') or not isinstance(f, models.AutoField) if (f.name == "id")
and not (getattr(f.remote_field, "parent_link", False)) or not isinstance(f, models.AutoField)
and not (getattr(f.remote_field, "parent_link", False))
] ]
@classmethod @classmethod
@ -239,9 +232,9 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
exclude = cls._meta.exclude exclude = cls._meta.exclude
assert not (fields is None and exclude is None), ( assert not (fields is None and exclude is None), (
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' "
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit " "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__ "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
) )
# Setting exclude with no fields implies all other fields. # Setting exclude with no fields implies all other fields.
@ -255,9 +248,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
# Remove excluded fields # Remove excluded fields
exclude = exclude or [] exclude = exclude or []
if not isinstance(fields, dict): if not isinstance(fields, dict):
fields = [ fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude]
(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude
]
else: else:
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] 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: if field is None:
undefined.append(field_name) undefined.append(field_name)
# 更新默认字符串搜索为模糊搜索 # 更新默认字符串搜索为模糊搜索
if isinstance(field, (models.CharField)) and filterset_fields == '__all__' and lookups == [ if (
'exact']: isinstance(field, (models.CharField))
lookups = ['icontains'] and filterset_fields == "__all__"
and lookups == ["exact"]
):
lookups = ["icontains"]
for lookup_expr in lookups: for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr) filter_name = cls.get_filter_name(field_name, lookup_expr)
@ -303,20 +297,15 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
continue continue
if field is not None: if field is not None:
filters[filter_name] = cls.filter_for_field( filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
field, field_name, lookup_expr
)
# Allow Meta.fields to contain declared filters *only* when a list/tuple # Allow Meta.fields to contain declared filters *only* when a list/tuple
if isinstance(cls._meta.fields, (list, tuple)): if isinstance(cls._meta.fields, (list, tuple)):
undefined = [ undefined = [f for f in undefined if f not in cls.declared_filters]
f for f in undefined if f not in cls.declared_filters
]
if undefined: if undefined:
raise TypeError( raise TypeError(
"'Meta.fields' must not contain non-model field names: %s" "'Meta.fields' must not contain non-model field names: %s" % ", ".join(undefined)
% ", ".join(undefined)
) )
# Add in declared filters. This is necessary since we don't enforce adding # 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: if not filterset.is_valid() and self.raise_exception:
raise utils.translate_validation(filterset.errors) raise utils.translate_validation(filterset.errors)
return filterset.qs 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

View File

@ -13,6 +13,7 @@ export const crudOptions = (vm) => {
height: '100%', // 表格高度100%, 使用toolbar必须设置 height: '100%', // 表格高度100%, 使用toolbar必须设置
highlightCurrentRow: false, highlightCurrentRow: false,
defaultExpandAll: true, defaultExpandAll: true,
resizable: true,
treeConfig: { treeConfig: {
transform: true, transform: true,
rowField: 'id', rowField: 'id',
@ -20,7 +21,13 @@ export const crudOptions = (vm) => {
hasChild: 'hasChild', hasChild: 'hasChild',
lazy: true, lazy: true,
loadMethod: ({ row }) => { 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 return ret.data.data
}) })
}, },
@ -28,6 +35,7 @@ export const crudOptions = (vm) => {
} }
}, },
rowHandle: { rowHandle: {
fixed: 'right',
width: 140, width: 140,
view: { view: {
thin: true, thin: true,
@ -55,7 +63,7 @@ export const crudOptions = (vm) => {
// 或者直接传true,不显示title不居中 // 或者直接传true,不显示title不居中
title: '序号', title: '序号',
align: 'center', align: 'center',
width: 100 width: 70
}, },
viewOptions: { viewOptions: {
@ -71,7 +79,7 @@ export const crudOptions = (vm) => {
show: false, show: false,
disabled: true, disabled: true,
search: { search: {
disabled: false disabled: true
}, },
form: { form: {
disabled: true, disabled: true,
@ -136,8 +144,8 @@ export const crudOptions = (vm) => {
} }
} }
}, },
width: 180,
type: 'input', type: 'input',
showOverflow: 'tooltip',
form: { form: {
rules: [ rules: [
// 表单校验规则 // 表单校验规则

View File

@ -275,7 +275,7 @@ export const crudOptions = (vm) => {
return request({ return request({
url: url url: url
}).then(ret => { }).then(ret => {
return ret.data.data return ret.data
}) })
} }
}, },