diff --git a/backend/.idea/modules.xml b/backend/.idea/modules.xml
new file mode 100644
index 0000000..1f3c69f
--- /dev/null
+++ b/backend/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dvadmin-backend/application/settings.py b/dvadmin-backend/application/settings.py
index 0f4be4a..1eaf059 100644
--- a/dvadmin-backend/application/settings.py
+++ b/dvadmin-backend/application/settings.py
@@ -46,6 +46,8 @@ INSTALLED_APPS = [
'captcha',
# 自定义app
'apps.permission',
+ 'apps.op_drf',
+ 'apps.system',
]
MIDDLEWARE = [
diff --git a/dvadmin-backend/application/urls.py b/dvadmin-backend/application/urls.py
index c551e84..56aff56 100644
--- a/dvadmin-backend/application/urls.py
+++ b/dvadmin-backend/application/urls.py
@@ -13,7 +13,6 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
-import json
from captcha.conf import settings as ca_settings
from captcha.helpers import captcha_image_url, captcha_audio_url
@@ -25,9 +24,9 @@ from django.urls import re_path, include
from django.views.static import serve
from rest_framework.views import APIView
-from apps.permission.views import GetUserView, GetRouters
+from apps.permission.views import GetUserProfileView, GetRouters
+from apps.op_drf.response import SuccessResponse
from utils.login import LoginView, LogoutView
-from utils.response import SuccessResponse
class CaptchaRefresh(APIView):
@@ -47,10 +46,12 @@ class CaptchaRefresh(APIView):
urlpatterns = [
re_path('api-token-auth/', LoginView.as_view(), name='api_token_auth'),
re_path(r'^admin/', admin.site.urls),
+ re_path(r'^permission/', include('apps.permission.urls')),
+ re_path(r'^system/', include('apps.system.urls')),
re_path(r'media/(?P.*)', serve, {"document_root": settings.MEDIA_ROOT}),
re_path(r'^login/$', LoginView.as_view()),
re_path(r'^logout/$', LogoutView.as_view()),
- re_path(r'^getInfo/$', GetUserView.as_view()),
+ re_path(r'^getInfo/$', GetUserProfileView.as_view()),
re_path(r'^getRouters/$', GetRouters.as_view()),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r"captcha/refresh/$", CaptchaRefresh.as_view(), name="captcha-refresh"), # 刷新验证码
diff --git a/dvadmin-backend/apps/op_drf/__init__.py b/dvadmin-backend/apps/op_drf/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/dvadmin-backend/apps/op_drf/apps.py b/dvadmin-backend/apps/op_drf/apps.py
new file mode 100644
index 0000000..d937d12
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/apps.py
@@ -0,0 +1,13 @@
+from django.apps import AppConfig
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class OpDrfConfig(AppConfig):
+ name = 'op_drf'
+ verbose_name = "OP DRF"
+
+ def ready(self):
+ logging.info("OP DRF框架检测完成:success")
diff --git a/dvadmin-backend/utils/fields.py b/dvadmin-backend/apps/op_drf/fields.py
similarity index 53%
rename from dvadmin-backend/utils/fields.py
rename to dvadmin-backend/apps/op_drf/fields.py
index 64b8dc1..77478d3 100644
--- a/dvadmin-backend/utils/fields.py
+++ b/dvadmin-backend/apps/op_drf/fields.py
@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import SET_NULL
-from .string_util import uuid_8, uuid_16, uuid_32, uuid_36
+from utils.string_util import uuid_8, uuid_16, uuid_32, uuid_36
class IdField(models.CharField):
@@ -70,7 +70,8 @@ class DescriptionField(models.TextField):
"""
def __init__(self, *args, **kwargs):
- kwargs['default'] = kwargs.get('default', '')
+ if kwargs.get('null', True):
+ kwargs['default'] = kwargs.get('default', '')
kwargs['blank'] = kwargs.get('blank', True)
kwargs['null'] = kwargs.get('null', True)
kwargs['verbose_name'] = kwargs.get('verbose_name', '描述')
@@ -78,136 +79,6 @@ class DescriptionField(models.TextField):
super().__init__(*args, **kwargs)
-class TextField(models.TextField):
- """
- xxx = TextField()
- """
-
- def __init__(self, *args, **kwargs):
- kwargs['default'] = kwargs.get('default', '')
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- super().__init__(*args, **kwargs)
-
-
-class CharField(models.CharField):
- """
- xxx = CharField()
- """
-
- def __init__(self, *args, **kwargs):
- kwargs['default'] = kwargs.get('default', '')
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- super().__init__(*args, **kwargs)
-
-
-class IntegerField(models.IntegerField):
- """
- xxx = IntegerField()
- """
-
- def __init__(self, *args, **kwargs):
- kwargs['default'] = kwargs.get('default', 0)
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- super().__init__(*args, **kwargs)
-
-
-class BooleanField(models.BooleanField):
- """
- xxx = BooleanField()
- """
-
- def __init__(self, *args, **kwargs):
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- super().__init__(*args, **kwargs)
-
-
-class DateField(models.DateField):
- """
- xxx = DateField()
- """
-
- def __init__(self, *args, **kwargs):
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- kwargs['editable'] = kwargs.get('default', False)
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
- super().__init__(*args, **kwargs)
-
-
-class DateTimeField(models.DateTimeField):
- """
- xxx = DateTimeField()
- """
-
- def __init__(self, *args, **kwargs):
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- kwargs['editable'] = kwargs.get('default', False)
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
- super().__init__(*args, **kwargs)
-
-
-class ForeignKey(models.ForeignKey):
- """
- xxx = ForeignKey()
- """
-
- def __init__(self, to=None, on_delete=None, related_name=None, related_query_name=None, limit_choices_to=None,
- parent_link=False, to_field=None, db_constraint=False, **kwargs):
- if on_delete is None:
- on_delete = SET_NULL
- if to_field is None:
- to_field = 'id'
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- kwargs['editable'] = kwargs.get('default', False)
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
- super().__init__(to, on_delete, related_name, related_query_name, limit_choices_to, parent_link, to_field,
- db_constraint, **kwargs)
-
-
-class OneToOneField(models.OneToOneField):
- """
- xxx = OneToOneField()
- """
-
- def __init__(self, *args, on_delete=None, to_field=None, db_constraint=False, **kwargs):
- if on_delete is None:
- on_delete = SET_NULL
- if to_field is None:
- to_field = 'id'
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- kwargs['editable'] = kwargs.get('default', None)
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
- super().__init__(*args, on_delete=on_delete, to_field=to_field, db_constraint=db_constraint, **kwargs)
-
-
-class ManyToManyField(models.ManyToManyField):
- """
- xxx = ManyToManyField()
- """
-
- def __init__(self, *args, db_constraint=False, **kwargs):
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
- kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '')
- kwargs['editable'] = kwargs.get('default', False)
- kwargs['blank'] = kwargs.get('blank', True)
- super().__init__(*args, db_constraint=db_constraint, **kwargs)
-
-
class UserForeignKeyField(models.ForeignKey):
"""
user = UserForeignKeyField()
@@ -224,8 +95,6 @@ class UserForeignKeyField(models.ForeignKey):
kwargs['verbose_name'] = kwargs.get('verbose_name', '关联的用户')
kwargs['help_text'] = kwargs.get('help_text', '') or kwargs.get('verbose_name', '关联的用户')
kwargs['editable'] = kwargs.get('default', False)
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['null'] = kwargs.get('null', True)
super().__init__(to, on_delete, related_name, related_query_name, limit_choices_to, parent_link, to_field,
db_constraint, **kwargs)
@@ -258,7 +127,7 @@ class CreateDateTimeField(models.DateTimeField):
super().__init__(verbose_name, name, auto_now, auto_now_add, **kwargs)
-class CreatorCharField(CharField):
+class CreatorCharField(models.CharField):
"""
creator = CreatorCharField()
"""
@@ -272,7 +141,7 @@ class CreatorCharField(CharField):
super().__init__(*args, **kwargs)
-class ModifierCharField(CharField):
+class ModifierCharField(models.CharField):
"""
modifier = ModifierCharField()
"""
diff --git a/dvadmin-backend/apps/op_drf/filters.py b/dvadmin-backend/apps/op_drf/filters.py
new file mode 100644
index 0000000..0ac31a8
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/filters.py
@@ -0,0 +1,96 @@
+"""
+常用的过滤器以及DRF的过滤器
+"""
+import json
+import logging
+import operator
+from functools import reduce
+
+from django.utils import six
+from mongoengine.queryset import visitor
+from rest_framework.filters import BaseFilterBackend, SearchFilter, OrderingFilter
+
+logger = logging.getLogger(__name__)
+
+
+def get_as_kwargs(request):
+ params = request.GET.dict()
+ if 'as' not in params:
+ return {}
+ as_params = json.loads(params.get('as', '{}'))
+ return as_params
+
+
+class MongoSearchFilter(SearchFilter):
+ """
+ 适配Mongo模型视图的Search过滤器
+ """
+
+ def filter_queryset(self, request, queryset, view):
+ search_fields = getattr(view, 'search_fields', None)
+ search_terms = self.get_search_terms(request)
+ if not search_fields or not search_terms:
+ return queryset
+ orm_lookups = [
+ self.construct_search(six.text_type(search_field))
+ for search_field in search_fields
+ ]
+ if not orm_lookups:
+ return queryset
+ conditions = []
+ for search_term in search_terms:
+ queries = [
+ visitor.Q(**{orm_lookup: search_term})
+ for orm_lookup in orm_lookups
+ ]
+ conditions.append(reduce(operator.or_, queries))
+ queryset = queryset.filter(reduce(operator.and_, conditions))
+ return queryset
+
+
+class MongoOrderingFilter(OrderingFilter):
+ """
+ 适配Mongo模型视图的Search过滤器
+ """
+
+ def get_valid_fields(self, queryset, view, context={}):
+ valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
+ if valid_fields is None:
+ return self.get_default_valid_fields(queryset, view, context)
+ elif valid_fields == '__all__':
+ # View explicitly allows filtering on any model field
+ model = view.get_serializer().__class__.Meta.model
+ valid_fields = [
+ (field_name, getattr(field, 'verbose_name', field_name)) for field_name, field in model._fields.items()
+ ]
+ else:
+ valid_fields = [
+ (item, item) if isinstance(item, six.string_types) else item
+ for item in valid_fields
+ ]
+
+ return valid_fields
+
+
+class AdvancedSearchFilter(BaseFilterBackend):
+ """
+ 高级搜索过滤器
+ """
+
+ def filter_queryset(self, request, queryset, view):
+ as_kwargs = get_as_kwargs(request)
+ if as_kwargs:
+ queryset = queryset.filter(**as_kwargs)
+ return queryset
+
+
+class MongoAdvancedSearchFilter(BaseFilterBackend):
+ """
+ mongo高级搜索过滤器
+ """
+
+ def filter_queryset(self, request, queryset, view):
+ as_kwargs = get_as_kwargs(request)
+ if as_kwargs:
+ queryset = queryset.filter(**as_kwargs)
+ return queryset
diff --git a/dvadmin-backend/apps/op_drf/generics.py b/dvadmin-backend/apps/op_drf/generics.py
new file mode 100644
index 0000000..b25f127
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/generics.py
@@ -0,0 +1,465 @@
+from django.core.exceptions import ValidationError
+from django.db.models.query import QuerySet
+from django.http import Http404
+from django.shortcuts import get_object_or_404 as _get_object_or_404
+from rest_framework.settings import api_settings
+
+from . import mixins
+from .pagination import Pagination, JsonPagination
+from .response import SuccessResponse
+from utils.jsonpath_util import get_jsonpath, filter_json, search_json
+from utils.sort_util import sortList
+from .views import CustomAPIView
+
+
+def get_object_or_404(queryset, *filter_args, **filter_kwargs):
+ """
+ Same as Django's standard shortcut, but make sure to also raise 404
+ if the filter_kwargs don't match the required types.
+ """
+ try:
+ return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
+ except (TypeError, ValueError, ValidationError):
+ raise Http404
+
+
+class GenericAPIView(CustomAPIView):
+ """
+ Base class for all other generic views.
+ """
+ # You'll need to either set these attributes,
+ # or override `get_queryset()`/`get_serializer_class()`.
+ # If you are overriding a view method, it is important that you call
+ # `get_queryset()` instead of accessing the `queryset` property directly,
+ # as `queryset` will get evaluated only once, and those results are cached
+ # for all subsequent requests.
+ queryset = None
+ serializer_class = None
+
+ # If you want to use object lookups other than pk, set 'lookup_field'.
+ # For more complex lookup requirements override `get_object()`.
+ lookup_field = 'pk'
+ lookup_url_kwarg = None
+
+ # The filter backend classes to use for queryset filtering
+ filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
+
+ # The style to use for queryset pagination.
+ pagination_class = Pagination
+
+ def get_queryset(self):
+ """
+ Get the list of items for this view.
+ This must be an iterable, and may be a queryset.
+ Defaults to using `self.queryset`.
+
+ This method should always be used rather than accessing `self.queryset`
+ directly, as `self.queryset` gets evaluated only once, and those results
+ are cached for all subsequent requests.
+
+ You may want to override this if you need to provide different
+ querysets depending on the incoming request.
+
+ (Eg. return a list of items that is specific to the user)
+ """
+ assert self.queryset is not None, (
+ "'%s' should either include a `queryset` attribute, "
+ "or override the `get_queryset()` method."
+ % self.__class__.__name__
+ )
+
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated on each request.
+ queryset = queryset.all()
+ return queryset
+
+ def get_object(self):
+ """
+ Returns the object the view is displaying.
+
+ You may want to override this if you need to provide non-standard
+ queryset lookups. Eg if objects are referenced using multiple
+ keyword arguments in the url conf.
+ """
+ queryset = self.filter_queryset(self.get_queryset())
+
+ # Perform the lookup filtering.
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+
+ assert lookup_url_kwarg in self.kwargs, (
+ 'Expected view %s to be called with a URL keyword argument '
+ 'named "%s". Fix your URL conf, or set the `.lookup_field` '
+ 'attribute on the view correctly.' %
+ (self.__class__.__name__, lookup_url_kwarg)
+ )
+
+ filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
+ obj = get_object_or_404(queryset, **filter_kwargs)
+
+ # May raise a permission denied
+ self.check_object_permissions(self.request, obj)
+
+ return obj
+
+ def get_serializer(self, *args, **kwargs):
+ """
+ Return the serializer instance that should be used for validating and
+ deserializing input, and for serializing output.
+ """
+ serializer_class = self.get_serializer_class()
+ kwargs['context'] = self.get_serializer_context()
+ return serializer_class(*args, **kwargs)
+
+ def get_serializer_class(self):
+ """
+ Return the class to use for the serializer.
+ Defaults to using `self.serializer_class`.
+
+ You may want to override this if you need to provide different
+ serializations depending on the incoming request.
+
+ (Eg. admins get full serialization, others get basic serialization)
+ """
+ assert self.serializer_class is not None, (
+ "'%s' should either include a `serializer_class` attribute, "
+ "or override the `get_serializer_class()` method."
+ % self.__class__.__name__
+ )
+
+ return self.serializer_class
+
+ def get_serializer_context(self):
+ """
+ Extra context provided to the serializer class.
+ """
+ return {
+ 'request': self.request,
+ 'format': self.format_kwarg,
+ 'view': self
+ }
+
+ def filter_queryset(self, queryset):
+ """
+ Given a queryset, filter it with whichever filter backend is in use.
+
+ You are unlikely to want to override this method, although you may need
+ to call it either from a list view, or from a custom `get_object`
+ method if you want to apply the configured filtering backend to the
+ default queryset.
+ """
+ for backend in list(self.filter_backends):
+ queryset = backend().filter_queryset(self.request, queryset, self)
+ return queryset
+
+ @property
+ def paginator(self):
+ """
+ The paginator instance associated with the view, or `None`.
+ """
+ if not hasattr(self, '_paginator'):
+ if self.pagination_class is None:
+ self._paginator = None
+ else:
+ self._paginator = self.pagination_class()
+ return self._paginator
+
+ def paginate_queryset(self, queryset):
+ """
+ Return a single page of results, or `None` if pagination is disabled.
+ """
+ if self.paginator is None:
+ return None
+ return self.paginator.paginate_queryset(queryset, self.request, view=self)
+
+ def get_paginated_response(self, data):
+ """
+ Return a paginated style `Response` object for the given output data.
+ """
+ assert self.paginator is not None
+ return self.paginator.get_paginated_response(data)
+
+
+# Concrete view classes that provide method handlers
+# by composing the mixin classes with the base view.
+
+class CreateAPIView(mixins.CreateModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for creating a model instance.
+ """
+
+ def post(self, request, *args, **kwargs):
+ return self.create(request, *args, **kwargs)
+
+
+class ListAPIView(mixins.ListModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for listing a queryset.
+ """
+
+ def get(self, request, *args, **kwargs):
+ return self.list(request, *args, **kwargs)
+
+
+class RetrieveAPIView(mixins.RetrieveModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for retrieving a model instance.
+ """
+
+ def get(self, request, *args, **kwargs):
+ return self.retrieve(request, *args, **kwargs)
+
+
+class DestroyAPIView(mixins.DestroyModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for deleting a model instance.
+ """
+
+ def delete(self, request, *args, **kwargs):
+ return self.destroy(request, *args, **kwargs)
+
+
+class UpdateAPIView(mixins.UpdateModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for updating a model instance.
+ """
+
+ def put(self, request, *args, **kwargs):
+ return self.update(request, *args, **kwargs)
+
+ def patch(self, request, *args, **kwargs):
+ return self.partial_update(request, *args, **kwargs)
+
+
+class ListCreateAPIView(mixins.ListModelMixin,
+ mixins.CreateModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for listing a queryset or creating a model instance.
+ """
+
+ def get(self, request, *args, **kwargs):
+ return self.list(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ return self.create(request, *args, **kwargs)
+
+
+class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for retrieving, updating a model instance.
+ """
+
+ def get(self, request, *args, **kwargs):
+ return self.retrieve(request, *args, **kwargs)
+
+ def put(self, request, *args, **kwargs):
+ return self.update(request, *args, **kwargs)
+
+ def patch(self, request, *args, **kwargs):
+ return self.partial_update(request, *args, **kwargs)
+
+
+class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
+ mixins.DestroyModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for retrieving or deleting a model instance.
+ """
+
+ def get(self, request, *args, **kwargs):
+ return self.retrieve(request, *args, **kwargs)
+
+ def delete(self, request, *args, **kwargs):
+ return self.destroy(request, *args, **kwargs)
+
+
+class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ GenericAPIView):
+ """
+ Concrete view for retrieving, updating or deleting a model instance.
+ """
+
+ def get(self, request, *args, **kwargs):
+ return self.retrieve(request, *args, **kwargs)
+
+ def put(self, request, *args, **kwargs):
+ return self.update(request, *args, **kwargs)
+
+ def patch(self, request, *args, **kwargs):
+ return self.partial_update(request, *args, **kwargs)
+
+ def delete(self, request, *args, **kwargs):
+ return self.destroy(request, *args, **kwargs)
+
+
+class JsonListView(GenericAPIView):
+ """
+ JsonList的查询、过滤、搜索、排序通用视图
+ """
+ # 模糊搜索
+ search_param = api_settings.SEARCH_PARAM
+ search_fields = []
+
+ ordering_param = api_settings.ORDERING_PARAM
+ # 可排序属性,默认所有属性可排序
+ ordering_fields = '__all__'
+
+ pagination_class = JsonPagination
+ queryset: list = []
+ pagination_total = 0
+
+ # 返回的属性
+ response_fields = '__all__'
+ # response_fields = ['app_code', 'ip', 'hostname', 'host_type', 'host_env']
+ # 默认排序, 只支持view中设置ordering, 暂不支持排序参数由请求动态传入
+ ordering = None # 正序
+ # ordering = '-ip' # 倒序
+
+ _has_ordering = False
+ _exclude_params = []
+ # 属性
+ _json_fields = None
+
+ @property
+ def paginator(self):
+ if not hasattr(self, '_paginator'):
+ if self.pagination_class is None:
+ self._paginator = None
+ else:
+ self._paginator = self.pagination_class(len(self.get_queryset()))
+ self._paginator.request = self.request
+ return self._paginator
+
+ def filter_queryset(self, queryset):
+ """
+ 使用jsonpath实现, 重写对查询集的条件过滤逻辑
+ :param queryset:
+ :return:
+ """
+ if not self._exclude_params:
+ self.__class__._exclude_params = []
+ self.__class__._exclude_params.append(self.paginator.page_size_query_param)
+ self.__class__._exclude_params.append(self.paginator.page_query_param)
+ self.__class__._exclude_params.extend(self.paginator.other_page_size_query_param)
+ self.__class__._exclude_params.extend(self.paginator.other_page_query_param)
+ self.__class__._exclude_params.append(self.ordering_param)
+ self.__class__._exclude_params.append(self.search_param)
+ expr = get_jsonpath(self.request.query_params, self._exclude_params, self.json_fields)
+ if not expr:
+ return queryset
+ _queryset = filter_json(self.get_queryset(), expr)
+ if not _queryset:
+ _queryset = []
+ return _queryset
+
+ def get_queryset(self):
+ """
+ 获取查询集/初始化查询集
+ :return:
+ """
+ return self.queryset
+
+ @property
+ def json_fields(self):
+ if self._json_fields is None:
+ queryset = self.get_queryset()
+ if len(queryset):
+ ele: dict = queryset[0]
+ self.__class__._json_fields = {key: value.__class__.__name__ for (key, value) in ele.items()}
+ else:
+ self.__class__._json_fields = []
+ return self._json_fields
+
+ def queryset_ordering(self):
+ """
+ 查询集排序
+ :return:
+ """
+ if self._has_ordering:
+ return
+ _ordering = self.ordering
+ if _ordering and self.ordering.startswith('-'):
+ _ordering = self.ordering[1:]
+ if _ordering and _ordering in self.json_fields:
+ if (isinstance(self.ordering_fields,
+ str) and self.ordering_fields.lower() == '__all__') or _ordering in self.ordering_fields:
+ sortList(self.get_queryset(), self.ordering)
+ self.__class__._has_ordering = True
+
+ def queryset_search(self, queryset):
+ search_value = self.request.query_params.get(self.search_param, None)
+ if self.search_fields and search_value:
+ queryset = search_json(queryset, search_value, self.search_fields)
+ return queryset
+
+ def paginate_queryset(self, queryset):
+ """
+ 重写内存分页逻辑
+ :param queryset:
+ :return:
+ """
+ paginator: JsonPagination = self.paginator
+ if paginator is None:
+ return queryset
+ request = self.request
+ page_size = paginator.get_page_size(request)
+ page_num = paginator.get_page_num(request)
+ start_index = (page_num - 1) * page_size
+ end_index = start_index + page_size
+ page_expr = f"$[{start_index}:{end_index}]"
+ queryset = filter_json(queryset, page_expr)
+ return queryset
+
+ def get_paginated_response(self, data):
+ """
+ 重写分页的返回格式
+ :param data:
+ :return:
+ """
+ if self.paginator is None:
+ return SuccessResponse(data)
+ return self.paginator.get_paginated_response(data, self.pagination_total)
+
+ def filter_response_fields(self, data):
+ """
+ 过滤掉不显示的字段
+ :param data:
+ :return:
+ """
+ if isinstance(self.response_fields, str) and self.response_fields.lower() == '__all__':
+ return data
+ data = [{key: value for key, value in ele.items() if key in self.response_fields} for ele in data]
+ return data
+
+ def filter_json(self):
+ """
+ 过滤
+ (1)查询集排序, 根据ordering对查询集进行正序/倒序
+ (2)查询集过滤,根据请求参数传入的属性值
+ (3)分页, 如果存在分页器, 根据输入的分页参数分页
+ (4)属性字段过滤, 过滤掉不需要返回的属性值
+ :return:
+ """
+ self.queryset_ordering()
+ queryset = self.get_queryset()
+ queryset = self.filter_queryset(queryset)
+ queryset = self.queryset_search(queryset)
+ self.paginator.data_count = len(queryset)
+
+ queryset = self.paginate_queryset(queryset)
+ data = self.filter_response_fields(queryset)
+ return data
+
+ def get(self, request, *args, **kwargs):
+ data = self.filter_json()
+ return self.get_paginated_response(data)
diff --git a/dvadmin-backend/apps/op_drf/logging/handlers.py b/dvadmin-backend/apps/op_drf/logging/handlers.py
new file mode 100644
index 0000000..a3dc12d
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/logging/handlers.py
@@ -0,0 +1,10 @@
+import logging
+from django.core.cache import cache
+
+
+class RedisHandler(logging.StreamHandler):
+
+ def emit(self, record):
+ msg = self.format(record)
+ print(msg)
+
diff --git a/dvadmin-backend/apps/op_drf/logging/view_logger.py b/dvadmin-backend/apps/op_drf/logging/view_logger.py
new file mode 100644
index 0000000..2435711
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/logging/view_logger.py
@@ -0,0 +1,192 @@
+import logging
+
+from django.db.models import Model
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+logger = logging.getLogger(__name__)
+
+
+class ViewLogger(object):
+ """
+ 基于View视图的日志
+ """
+
+ def __init__(self, view=None, request=None, *args, **kwargs) -> None:
+ super().__init__()
+ self.view = view
+ self.request = request
+ self.log_prefix: str = ''
+
+ def handle(self, request: Request, *args, **kwargs):
+ pass
+
+
+class APIViewLogger(ViewLogger):
+ """
+ (1)仅在op_drf.views.CustomAPIView的子类中生效
+ (2)使用: 请勿直接配置view_logger_classes = (APIViewLogger, ), 这样无效,
+ 如有需求, 需要继承APIViewLogger重写其相应的方法
+ (3)重写handle()方法,所有请求均触发此方法
+ 重写handle_get()方法,仅GET请求均触发此方法
+ 重写handle_post()方法,仅POST请求均触发此方法
+ 重写handle_put()方法,仅PUT请求均触发此方法
+ 重写handle_delete()方法,仅DELETE请求均触发此方法
+ 重写handle_xxx()方法,仅xxx请求均触发此方法
+ """
+
+ def __init__(self, view=None, request=None, *args, **kwargs) -> None:
+ super().__init__()
+ self.view: APIView = view
+ self.request: Request = request
+ self.user = request.user
+
+
+class ModelViewLogger(APIViewLogger):
+ """
+ 基础模型操作日志
+ (1)仅在op_drf.viewsets.GenericViewSet的子类中生效
+ (1)在CustomModelViewSet子类中配置: view_logger_classes = [ModelViewLogger, ]
+ (2)不要在op_drf中继续写具体的日志逻辑代码,
+ 如有需求, 应该继承ModelViewLogger并且重写相应的方法, 例如CustomerModelViewLogger(涉及到其他模块时不要将代码放入op_drf)
+ """
+
+ def __init__(self, view=None, request=None, *args, **kwargs) -> None:
+ super().__init__(view, request)
+
+ if hasattr(self.view.get_queryset(), 'model'):
+ self.model: Model = self.view.get_queryset().model
+ elif hasattr(self.view.get_serializer(), 'Meta') and hasattr(self.view.get_serializer().Meta, 'model'):
+ self.model: Model = self.view.get_serializer().Meta.model
+
+
+class RelationshipViewLogger(APIViewLogger):
+ """
+ 关联关系模型操作日志
+ (1)在ModelRelationshipView子类中配置: view_logger_classes = [RelationshipViewLogger, ]
+ (2)不要在op_drf中继续写具体的日志逻辑代码,
+ 如有需求, 应该继承RelationshipViewLogger并且重写相应的方法, 例如CustomerRelationshipViewLogger(涉及到其他模块时不要将代码放入op_drf)
+ """
+
+ def __init__(self, view=None, request=None, instanceId=None, *args, **kwargs) -> None:
+ super().__init__(view, request)
+ self.instanceId: str = instanceId
+
+ def handle(self, request: Request, *args, **kwargs):
+ """
+ 每一次请求都会触发此方法
+ """
+
+ pass
+
+
+class CustomerRelationshipViewLogger(RelationshipViewLogger):
+ """
+ (1)在ModelRelationshipView子类中配置: view_logger_classes = [CustomerRelationshipViewLogger, ]
+ """
+
+ def __init__(self, view=None, request=None, instanceId=None, *args, **kwargs) -> None:
+ super().__init__(view, request, instanceId, *args, **kwargs)
+ self.log_prefix: str = 'RelationshipView日志系统:'
+
+ def handle_get(self, request: Request, *args, **kwargs):
+ """
+ 仅GET请求才会触发此方法
+ """
+ pass
+
+ def handle_post(self, request: Request, *args, **kwargs):
+ """
+ 仅POST请求才会触发此方法
+ """
+ operator = self.user.username
+ model_name = getattr(self.view.model, '_meta').verbose_name
+ to_field_name = self.view.to_field_name
+ to_model_name = getattr(self.view.relationship_model, '_meta').verbose_name
+ logger.info(
+ f'{self.log_prefix}用户[username={operator}]新增, {model_name}实例[{to_field_name}={self.instanceId}]与{to_model_name}的关联关系')
+
+ def handle_put(self, request: Request, *args, **kwargs):
+ """
+ 仅PUT请求才会触发此方法
+ """
+ operator = self.user.username
+ model_name = getattr(self.view.model, '_meta').verbose_name
+ to_field_name = self.view.to_field_name
+ to_model_name = getattr(self.view.relationship_model, '_meta').verbose_name
+ logger.info(
+ f'{self.log_prefix}用户[username={operator}]重置, {model_name}实例[{to_field_name}={self.instanceId}]与{to_model_name}的关联关系')
+
+ def handle_delete(self, request: Request, *args, **kwargs):
+ """
+ 仅DELETE请求才会触发此方法
+ """
+ operator = self.user.username
+ model_name = getattr(self.view.model, '_meta').verbose_name
+ to_field_name = self.view.to_field_name
+ to_model_name = getattr(self.view.relationship_model, '_meta').verbose_name
+ logger.info(
+ f'{self.log_prefix}用户[username={operator}]移除, {model_name}实例[{to_field_name}={self.instanceId}]与{to_model_name}的关联关系')
+
+
+class CustomerModelViewLogger(ModelViewLogger):
+ def __init__(self, view=None, request=None, *args, **kwargs) -> None:
+ super().__init__(view, request, *args, **kwargs)
+ self.log_prefix: str = 'CustomModelViewSet日志系统:'
+
+ def handle(self, request: Request, *args, **kwargs):
+ pass
+
+ def handle_retrieve(self, request: Request, instance: Model = None, *args, **kwargs):
+ """
+ 仅retrieve(GET)请求才会触发此方法
+ """
+ pass
+ operator = self.user.username
+ model_name = getattr(self.model, '_meta').verbose_name
+ logger.info(f'{self.log_prefix}用户[username={operator}]检索{model_name}:[{instance}]')
+
+ def handle_list(self, request: Request, *args, **kwargs):
+ """
+ 仅list(GET)请求才会触发此方法
+ """
+ pass
+ operator = self.user.username
+ model_name = getattr(self.model, '_meta').verbose_name
+ logger.info(f'{self.log_prefix}用户[username={operator}]查询{model_name}')
+
+ def handle_create(self, request: Request, instance: Model = None, *args, **kwargs):
+ """
+ 仅create(POST)请求才会触发此方法
+ """
+ pass
+ operator = self.user.username
+ model_name = getattr(self.model, '_meta').verbose_name
+ logger.info(f'{self.log_prefix}用户[username={operator}]创建{model_name}:[{instance}]')
+
+ def handle_update(self, request: Request, instance: Model = None, *args, **kwargs):
+ """
+ 仅update(PUT)请求才会触发此方法
+ """
+ pass
+ operator = self.user.username
+ model_name = getattr(self.model, '_meta').verbose_name
+ logger.info(f'{self.log_prefix}用户[username={operator}]更新{model_name}:[{instance}]')
+
+ def handle_partial_update(self, request: Request, instance: Model = None, *args, **kwargs):
+ """
+ 仅partial_update(PATCH)请求才会触发此方法
+ """
+ pass
+ operator = self.user.username
+ model_name = getattr(self.model, '_meta').verbose_name
+ logger.info(f'{self.log_prefix}用户[username={operator}]部分更新{model_name}:[{instance}]')
+
+ def handle_destroy(self, request: Request, instance: Model = None, *args, **kwargs):
+ """
+ 仅destroy(DELETE)请求才会触发此方法
+ """
+ pass
+ operator = self.user.username
+ model_name = getattr(self.model, '_meta').verbose_name
+ logger.info(f'{self.log_prefix}用户[username={operator}]删除{model_name}:[{instance}]')
diff --git a/dvadmin-backend/apps/op_drf/middleware.py b/dvadmin-backend/apps/op_drf/middleware.py
new file mode 100644
index 0000000..f26ace2
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/middleware.py
@@ -0,0 +1,102 @@
+"""
+django中间件
+"""
+
+import json
+import datetime
+from django.utils.deprecation import MiddlewareMixin
+from mongoengine import DynamicDocument, StringField, IntField, DictField, DateTimeField
+from rest_framework_mongoengine.serializers import DocumentSerializer
+import logging
+from utils.decorators import exceptionHandler
+from utils.request_util import get_request_ip, get_request_data, get_request_path
+from .viewsets import CustomMongoModelViewSet
+from django.conf import settings
+logger = logging.getLogger(__name__)
+
+
+class ApiLog(DynamicDocument):
+ """
+ API访问日志的Mongo模型
+ """
+ request_ip = StringField(verbose_name="request_ip", help_text="请求IP")
+ request_username = StringField(verbose_name="request_username", help_text="请求username")
+ request_method = StringField(verbose_name="request_method", help_text="请求方法")
+ request_path = StringField(verbose_name="request_path", help_text="请求路径")
+ request_body = DictField(verbose_name="request_body", help_text="请求参数")
+ response_code = IntField(verbose_name="response_code", help_text="响应状态码")
+ response_reason = StringField(verbose_name="response_reason", help_text="响应简述")
+ access_time = DateTimeField(verbose_name="access_time", help_text="访问时间")
+
+
+class ApiLogSerializer(DocumentSerializer):
+ """
+ API访问日志的Mongo序列化器
+ """
+ class Meta:
+ model = ApiLog
+ fields = '__all__'
+
+
+class ApiLogModelViewSet(CustomMongoModelViewSet):
+ """
+ API访问日志的CRUD视图
+ """
+ queryset = ApiLog.objects.all()
+ serializer_class = ApiLogSerializer
+ search_fields = ('request_ip', 'request_username', 'request_method', 'response_reason', 'source_system')
+ ordering = '-access_time' # 默认排序
+
+
+class ApiLoggingMiddleware(MiddlewareMixin):
+ """
+ 用于记录API访问日志中间件
+ """
+ def __init__(self, get_response=None):
+ super().__init__(get_response)
+ self.enable = op_settings.get_api_log_setting().get('enable', False)
+ self.methods = op_settings.get_api_log_setting().get('methods', set())
+
+ @classmethod
+ @exceptionHandler()
+ def __handle_request(cls, request):
+ request.request_ip = get_request_ip(request)
+ request.request_data = get_request_data(request)
+ request.access_time = datetime.datetime.now()
+
+ @classmethod
+ @exceptionHandler(logger=logger)
+ def __handle_response(cls, request, response):
+ # request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性
+ body = getattr(request, 'request_data', {})
+ # 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数)
+ if isinstance(body, dict) and body.get('password', ''):
+ body['password'] = '*' * len(body['password'])
+ info = {
+ 'request_ip': getattr(request, 'request_ip', 'unknown'),
+ 'request_username': request.user.username,
+ 'request_method': request.method,
+ 'request_path': request.path,
+ 'request_body': body,
+ 'response_code': response.status_code,
+ 'response_reason': response.reason_phrase,
+ 'source_system': getattr(settings,'SOURCE_SYSTEM_NAME',None),
+ 'access_time': request.access_time.strftime('%Y-%m-%d %H:%M:%S'),
+ }
+ log = ApiLog(**info)
+ log.save()
+
+ def process_request(self, request):
+ self.__handle_request(request)
+
+ def process_response(self, request, response):
+ """
+ 主要请求处理完之后记录
+ :param request:
+ :param response:
+ :return:
+ """
+ if self.enable:
+ if self.methods == 'ALL' or request.method in self.methods:
+ self.__handle_response(request, response)
+ return response
diff --git a/dvadmin-backend/apps/op_drf/mixins.py b/dvadmin-backend/apps/op_drf/mixins.py
new file mode 100644
index 0000000..cd9aba3
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/mixins.py
@@ -0,0 +1,273 @@
+from rest_framework import mixins
+from rest_framework import serializers
+from rest_framework import status
+from rest_framework.relations import ManyRelatedField, RelatedField, PrimaryKeyRelatedField
+from rest_framework.request import Request
+
+from .response import SuccessResponse
+
+
+class CreateModelMixin(mixins.CreateModelMixin):
+ """
+ 继承、增强DRF的CreateModelMixin, 标准化其返回值
+ """
+ create_serializer_class = None
+
+ def create(self, request: Request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ self.perform_create(serializer)
+ if hasattr(self, 'handle_logging'):
+ self.handle_logging(request, instance=serializer.instance, *args, **kwargs)
+ headers = self.get_success_headers(serializer.data)
+ return SuccessResponse(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+
+ def perform_create(self, serializer):
+ super().perform_create(serializer)
+
+
+class ListModelMixin(mixins.ListModelMixin):
+ """
+ 继承、增强DRF的CreateModelMixin, 标准化其返回值
+ """
+ list_serializer_class = None
+
+ def list(self, request: Request, *args, **kwargs):
+ queryset = self.filter_queryset(self.get_queryset())
+ page = self.paginate_queryset(queryset)
+ if hasattr(self, 'handle_logging'):
+ self.handle_logging(request, *args, **kwargs)
+ if page is not None:
+ if getattr(self, 'values_queryset', None):
+ return self.get_paginated_response(page)
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+ if getattr(self, 'values_queryset', None):
+ return SuccessResponse(page)
+ serializer = self.get_serializer(queryset, many=True)
+ return SuccessResponse(serializer.data)
+
+
+class RetrieveModelMixin(mixins.RetrieveModelMixin):
+ """
+ 继承、增强DRF的CreateModelMixin, 标准化其返回值
+ """
+ retrieve_serializer_class = None
+
+ def retrieve(self, request: Request, *args, **kwargs):
+ instance = self.get_object()
+ serializer = self.get_serializer(instance)
+ if hasattr(self, 'handle_logging'):
+ self.handle_logging(request, instance=instance, *args, **kwargs)
+ return SuccessResponse(serializer.data)
+
+
+class UpdateModelMixin(mixins.UpdateModelMixin):
+ """
+ 继承、增强DRF的CreateModelMixin, 标准化其返回值
+ """
+ update_serializer_class = None
+
+ def update(self, request: Request, *args, **kwargs):
+ partial = kwargs.pop('partial', False)
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ self.perform_update(serializer)
+ if getattr(instance, '_prefetched_objects_cache', None):
+ instance._prefetched_objects_cache = {}
+ if hasattr(self, 'handle_logging'):
+ self.handle_logging(request, instance=instance, *args, **kwargs)
+ return SuccessResponse(serializer.data)
+
+ def partial_update(self, request, *args, **kwargs):
+ kwargs['partial'] = True
+ return self.update(request, *args, **kwargs)
+
+
+class DestroyModelMixin(mixins.DestroyModelMixin):
+ """
+ 继承、增强DRF的CreateModelMixin, 标准化其返回值
+ """
+ destroy_serializer_class = None
+
+ def destroy(self, request: Request, *args, **kwargs):
+ instance = self.get_object()
+ self.perform_destroy(instance)
+ if hasattr(self, 'handle_logging'):
+ self.handle_logging(request, instance=instance, *args, **kwargs)
+ return SuccessResponse(status=status.HTTP_204_NO_CONTENT)
+
+ def perform_destroy(self, instance):
+ instance.delete()
+
+
+class TableSerializerMixin:
+ table_option = None
+ extra_columns = []
+
+ FIELD_TYPE_MAP = {
+ 'AutoField': {
+ 'type': 'input',
+ 'addDisabled': True,
+ },
+
+ 'CharField': {
+ 'type': 'input',
+ "maxlength": 255
+ },
+
+ 'PasswordField': {
+ 'type': 'input',
+ 'maxlength': 255
+ },
+
+ 'URLField': {
+ 'type': 'input',
+ },
+
+ 'UUIDField': {
+ 'type': 'input',
+ 'minlength': 32,
+ 'maxlength': 32,
+ },
+
+ 'UUID8Field': {
+ 'type': 'input',
+ 'minlength': 8,
+ 'maxlength': 8,
+ },
+
+ 'UUID16Field': {
+ 'type': 'input',
+ 'minlength': 16,
+ 'maxlength': 16,
+ },
+
+ 'UUID32Field': {
+ 'type': 'input',
+ 'minlength': 32,
+ 'maxlength': 32,
+ },
+
+ 'UUID36Field': {
+ 'type': 'input',
+ 'minlength': 36,
+ 'maxlength': 36
+ },
+
+ 'DateTimeField': {
+ 'type': 'datetime',
+ 'format': "yyyy-MM-dd hh:mm:ss",
+ 'valueFormat': "yyyy-MM-dd hh:mm:ss",
+ },
+ 'DateField': {
+ 'type': 'date',
+ 'format': "yyyy-MM-dd",
+ 'valueFormat': "yyyy-MM-dd",
+ },
+
+ 'TimeField': {
+ 'type': 'time',
+ 'format': "hh:mm:ss",
+ 'valueFormat': "hh:mm:ss",
+ },
+
+ 'BooleanField': {
+ 'type': 'radio',
+ 'dicData': [
+ {'value': False, 'label': '否'},
+ {'value': True, 'label': '是'},
+ ]
+ },
+
+ 'ManyRelatedField': {
+ # 'type': 'select',
+ 'type': 'array',
+ # "multiple": True,
+ 'required': False,
+ },
+ }
+
+ FIELD_TYPE_DEFAULT = {
+ 'type': 'input',
+ }
+
+ def getTable(self, serializer: serializers.ModelSerializer = None):
+ if not serializer:
+ serializer = self.get_serializer()
+ serializer_class = serializer.__class__
+ model = serializer_class.Meta.model
+ title = model.__name__
+ if hasattr(model, 'Meta'):
+ if hasattr(model.Meta, 'verbose_name'):
+ title = model.Meta.verbose_name or ''
+ column = self.getColumn(serializer)
+ table = {
+ 'title': title,
+ 'page': True,
+ 'align': 'center',
+ 'menuAlign': 'center',
+ 'columnBtn': True,
+ 'menu': True,
+ 'menuType': 'icon',
+ 'addBtn': True,
+ 'delBtn': True,
+ 'editBtn': True,
+ 'column': column
+ }
+ return table
+
+ def getColumn(self, serializer: serializers.ModelSerializer = None):
+ if not serializer:
+ serializer = self.get_serializer()
+ serializer_class = serializer.__class__
+ fields = serializer.get_fields()
+ show_fields = getattr(serializer_class.Meta, 'show_fields', set())
+ hide_fields = getattr(serializer_class.Meta, 'hide_fields', set())
+ search_fields = getattr(serializer_class.Meta, 'search_fields', set())
+ sortable_fields = getattr(serializer_class.Meta, 'sortable_fields', set())
+ column = []
+ for prop in fields:
+ field = fields[prop]
+ field_type = field.__class__.__name__
+ info = {
+ 'prop': prop,
+ 'label': field.label or prop,
+ 'hide': hide_fields == '__all__' or prop in hide_fields,
+ 'search': search_fields == '__all__' or prop in search_fields,
+ 'sortable': sortable_fields == '__all__' or prop in sortable_fields,
+ 'width': 'auto',
+ 'align': 'left',
+ 'overHidden': False,
+ }
+ type_info = self.FIELD_TYPE_MAP.get(field_type, self.FIELD_TYPE_DEFAULT)
+ info.update(type_info)
+
+ allow_null = getattr(field, 'allow_null', False)
+ allow_blank = getattr(field, 'allow_blank', False)
+ allow_empty = getattr(field, 'allow_empty', False)
+
+ read_only = getattr(field, 'read_only', False)
+ write_only = getattr(field, 'write_only', False)
+
+ if not any([allow_null, allow_blank, allow_empty]):
+ rules = [{
+ 'required': True,
+ 'message': f"""请输入{info['label']}""",
+ 'trigger': "blur"
+ }]
+ info['rules'] = rules
+ if read_only:
+ info['editDisabled'] = True,
+ info['clearable'] = False
+
+ if not isinstance(field, (ManyRelatedField, RelatedField, PrimaryKeyRelatedField)):
+ # 防止序列化该字段的关系模型所有数据
+ choices = getattr(field, 'choices', None)
+ if choices:
+ dicData = list(map(lambda choice: {'value': choice[0], 'label': choice[1]}, choices.items()))
+ info['dicData'] = dicData
+ info['type'] = 'select'
+ column.append(info)
+ return column
diff --git a/dvadmin-backend/utils/BaseModels.py b/dvadmin-backend/apps/op_drf/models.py
similarity index 100%
rename from dvadmin-backend/utils/BaseModels.py
rename to dvadmin-backend/apps/op_drf/models.py
diff --git a/dvadmin-backend/apps/op_drf/pagination.py b/dvadmin-backend/apps/op_drf/pagination.py
new file mode 100644
index 0000000..99657f2
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/pagination.py
@@ -0,0 +1,119 @@
+from collections import OrderedDict
+
+from rest_framework.pagination import PageNumberPagination, _positive_int
+from rest_framework.utils.urls import replace_query_param
+
+from .response import SuccessResponse
+
+
+class Pagination(PageNumberPagination):
+ """
+ 标准分页器
+ """
+ page_size_query_param = 'pageSize'
+ other_page_size_query_param = []
+ # other_page_size_query_param = ['pageSize', ]
+
+ page_query_param = "pageNum"
+ other_page_query_param = []
+ # other_page_query_param = ['currentPage', 'pageNum']
+ max_page_size = 1000
+ page_size = 10
+
+ def paginate_queryset(self, queryset, request, view=None):
+ return super().paginate_queryset(queryset, request, view)
+
+ def get_next_link(self):
+ return super().get_next_link()
+
+ def get_previous_link(self):
+ return super().get_previous_link()
+
+ def get_page_size(self, request):
+ """
+ 获取页大小
+ :param request:
+ :return:
+ """
+ if self.other_page_size_query_param:
+ for param_name in self.other_page_size_query_param:
+ if param_name in request.query_params:
+ return _positive_int(
+ request.query_params[param_name],
+ strict=True,
+ cutoff=self.max_page_size
+ )
+ return super().get_page_size(request)
+
+ def get_page_num(self, request):
+ """
+ 获取页码
+ :param request:
+ :return:
+ """
+ if self.other_page_query_param:
+ for param_name in self.other_page_query_param:
+ if param_name in request.query_params:
+ return _positive_int(request.query_params[param_name], strict=True)
+ page_num = request.query_params.get(self.page_query_param, 1)
+ return _positive_int(page_num, strict=True)
+
+ def get_paginated_response(self, data, search_fields=''):
+ return SuccessResponse(OrderedDict([
+ ('count', self.page.paginator.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+
+class JsonPagination(Pagination):
+ """
+ Json数据内存分页器
+ """
+
+ def __init__(self, count=0) -> None:
+ super().__init__()
+ self._page_size = 0
+ self._page_num = 0
+ self._count = count
+ self.data_count = 0
+
+ def get_page_size(self, request):
+ _page_size = super().get_page_size(request)
+ self._page_size = _page_size
+ return _page_size
+
+ def get_page_num(self, request):
+ _page_num = super().get_page_num(request)
+ self._page_num = _page_num
+ return _page_num
+
+ def get_next_link(self):
+ if self._page_size * self._page_num >= self.data_count:
+ return None
+ url = self.request.build_absolute_uri()
+ url = replace_query_param(url, self.page_query_param, self._page_num + 1)
+ url = replace_query_param(url, self.page_size_query_param, self._page_size)
+ return url
+
+ def get_previous_link(self, count=0):
+ if self._page_num <= 1:
+ return None
+ url = self.request.build_absolute_uri()
+ url = replace_query_param(url, self.page_query_param, self._page_num - 1)
+ url = replace_query_param(url, self.page_size_query_param, self._page_size)
+ return url
+
+ def paginate_queryset(self, queryset, request, view=None):
+ self.get_page_size(request)
+ self.get_page_num(request)
+ return super().paginate_queryset(queryset, request, view)
+
+ def get_paginated_response(self, data, search_fields=''):
+ return SuccessResponse(OrderedDict([
+ ('count', self.data_count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
diff --git a/dvadmin-backend/apps/op_drf/response.py b/dvadmin-backend/apps/op_drf/response.py
new file mode 100644
index 0000000..0a486ee
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/response.py
@@ -0,0 +1,52 @@
+"""
+常用的Response以及Django的Response、DRF的Response
+"""
+from django.http.response import DjangoJSONEncoder
+from rest_framework.response import Response
+
+
+class OpDRFJSONEncoder(DjangoJSONEncoder):
+ """
+ 重写DjangoJSONEncoder
+ (1)默认返回支持中文格式的json字符串
+ """
+
+ def __init__(self, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
+ indent=None, separators=None, default=None):
+ super().__init__(skipkeys=skipkeys, ensure_ascii=False, check_circular=check_circular,
+ allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, separators=separators,
+ default=default)
+
+
+class SuccessResponse(Response):
+ """
+ 标准响应成功的返回, SuccessResponse(data)或者SuccessResponse(data=data)
+ (1)默认错误码返回200, 不支持指定其他返回码
+ """
+
+ def __init__(self, data=None, msg='success', status=None, template_name=None, headers=None, exception=False,
+ content_type=None):
+ std_data = {
+ "code": 200,
+ "data": data,
+ "msg": msg,
+ "status": 'success'
+ }
+ super().__init__(std_data, status, template_name, headers, exception, content_type)
+
+
+class ErrorResponse(Response):
+ """
+ 标准响应错误的返回,ErrorResponse(msg='xxx')
+ (1)默认错误码返回201, 也可以指定其他返回码:ErrorResponse(code=xxx)
+ """
+
+ def __init__(self, data=None, msg='error', code=201, status=None, template_name=None, headers=None,
+ exception=False, content_type=None):
+ std_data = {
+ "code": code,
+ "data": data,
+ "msg": msg,
+ "status": 'error'
+ }
+ super().__init__(std_data, status, template_name, headers, exception, content_type)
diff --git a/dvadmin-backend/apps/op_drf/result.py b/dvadmin-backend/apps/op_drf/result.py
new file mode 100644
index 0000000..82952c8
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/result.py
@@ -0,0 +1,72 @@
+"""
+标准返回
+"""
+
+
+def funcSuccess(data=None, msg='success', **kwargs):
+ """
+ 普通函数成功返回格式
+ :param data:
+ :param msg:
+ :return:
+ """
+ return {
+ "result": True,
+ "msg": msg,
+ "data": data,
+ }
+
+
+def funcError(data=None, msg='error', **kwargs):
+ """
+ 普通函数失败返回格式
+ :param data:
+ :param msg:
+ :return:
+ """
+ return {
+ "result": False,
+ "msg": msg,
+ "data": data,
+ }
+
+
+def funcResult(result=True, data=None, msg='success', **kwargs):
+ """
+ 普通函数返回格式
+ :param result:
+ :param data:
+ :param msg:
+ :return:
+ """
+ if result:
+ return funcSuccess(data=data, msg=msg)
+ return funcError(data=data, msg=msg)
+
+
+def paginate(data=None, count=0, next=None, previous=None, msg='success', code=2000):
+ """
+ 标准分页返回, 分页错误时:应该直接使用pagination()即可,无需传入任何参数
+ :param data: 默认为[]
+ :param count: 默认为len(data); 总计值, 不是data的元素个数,相当于count(*);仅当不传入count时, count==len(data)
+ :param next: 默认为None, 下一页
+ :param previous: 默认为None, 上一页
+ :param msg: 默认为success
+ :param code: 默认为2000, 建议不传入参数,使用默认即可
+ :return:
+ """
+ if not data:
+ data = []
+ if not count:
+ count = len(data)
+ return {
+ "code": code,
+ "data": {
+ "count": count,
+ "next": next,
+ "previous": previous,
+ "results": data
+ },
+ "msg": msg,
+ "status": "success"
+ }
diff --git a/dvadmin-backend/apps/op_drf/serializers.py b/dvadmin-backend/apps/op_drf/serializers.py
new file mode 100644
index 0000000..ef693f6
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/serializers.py
@@ -0,0 +1,93 @@
+from rest_framework.serializers import ModelSerializer
+from rest_framework.fields import empty
+from rest_framework.request import Request
+
+
+class CustomModelSerializer(ModelSerializer):
+ """
+ 增强DRF的ModelSerializer,可自动更新模型的审计字段记录
+ (1)仅当op_drf.generics.GenericAPIView的子类里使用时有效
+ (2)非op_drf.generics.GenericAPIView里使用时, 与ModelSerializer作用一样,没人任何增强
+ (3)self.request能获取到rest_framework.request.Request对象
+ """
+ # 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
+ modifier_field_name = 'modifier'
+ # 创建人的审计字段名称, 默认creator, 继承使用时可自定义覆盖
+ creator_field_name = 'creator'
+
+ def __init__(self, instance=None, data=empty, request=None, **kwargs):
+ super().__init__(instance, data, **kwargs)
+ self.request: Request = request
+
+ def save(self, **kwargs):
+ return super().save(**kwargs)
+
+ def create(self, validated_data):
+ if self.request:
+ username = self.get_request_username()
+ if self.modifier_field_name in self.fields.fields:
+ validated_data[self.modifier_field_name] = username
+ if self.creator_field_name in self.fields.fields:
+ validated_data[self.creator_field_name] = username
+ return super().create(validated_data)
+
+ def update(self, instance, validated_data):
+ if self.request:
+ if hasattr(self.instance, self.modifier_field_name):
+ self.instance.modifier = self.get_request_username()
+ return super().update(instance, validated_data)
+
+ def get_request_username(self):
+ if getattr(self.request, 'user', None):
+ return getattr(self.request.user, 'username', None)
+ return None
+
+
+
+ @property
+ def fields(self):
+ fields = super().fields
+
+ if not hasattr(self, '_context'):
+ return fields
+ is_root = self.root == self
+ parent_is_list_root = self.parent == self.root and getattr(self.parent, 'many', False)
+ if not (is_root or parent_is_list_root):
+ return fields
+
+ try:
+ request = self.request or self.context['request']
+ except KeyError:
+ return fields
+ params = getattr(
+ request, 'query_params', getattr(request, 'GET', None)
+ )
+ if params is None:
+ pass
+ try:
+ filter_fields = params.get('_fields', None).split(',')
+ except AttributeError:
+ filter_fields = None
+
+ try:
+ omit_fields = params.get('_omit', None).split(',')
+ except AttributeError:
+ omit_fields = []
+
+ existing = set(fields.keys())
+ if filter_fields is None:
+ allowed = existing
+ else:
+ allowed = set(filter(None, filter_fields))
+
+ omitted = set(filter(None, omit_fields))
+ for field in existing:
+ if field not in allowed:
+ fields.pop(field, None)
+ if field in omitted:
+ fields.pop(field, None)
+
+ return fields
+
+
+
diff --git a/dvadmin-backend/apps/op_drf/urls.py b/dvadmin-backend/apps/op_drf/urls.py
new file mode 100644
index 0000000..e69de29
diff --git a/dvadmin-backend/apps/op_drf/views.py b/dvadmin-backend/apps/op_drf/views.py
new file mode 100644
index 0000000..1e46a79
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/views.py
@@ -0,0 +1,287 @@
+import logging
+import traceback
+from types import FunctionType, MethodType
+
+from rest_framework.exceptions import APIException as DRFAPIException
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from utils import exceptions
+from utils.model_util import ModelRelateUtils
+from .logging.view_logger import CustomerRelationshipViewLogger
+from .response import SuccessResponse, ErrorResponse
+from .serializers import CustomModelSerializer
+
+logger = logging.getLogger(__name__)
+
+
+def op_exception_handler(ex, context):
+ """
+ 统一异常拦截处理
+ 目的:(1)取消所有的500异常响应,统一响应为标准错误返回
+ (2)准确显示错误信息
+ :param ex:
+ :param context:
+ :return:
+ """
+ msg = ''
+ if isinstance(ex, DRFAPIException):
+ # set_rollback()
+ msg = ex.detail
+ elif isinstance(ex, exceptions.APIException):
+ msg = ex.message
+ elif isinstance(ex, Exception):
+ logger.error(traceback.format_exc())
+ msg = str(ex)
+ return ErrorResponse(msg=msg)
+
+
+class CustomAPIView(APIView):
+ """
+ 继承、增强DRF的APIView
+ """
+ extra_permission_classes = ()
+ # 仅当GET方法时会触发该权限的校验
+ GET_permission_classes = ()
+
+ # 仅当POST方法时会触发该权限的校验
+ POST_permission_classes = ()
+
+ # 仅当DELETE方法时会触发该权限的校验
+ DELETE_permission_classes = ()
+
+ # 仅当PUT方法时会触发该权限的校验
+ PUT_permission_classes = ()
+
+ view_logger_classes = ()
+
+ def initial(self, request: Request, *args, **kwargs):
+ super().initial(request, *args, **kwargs)
+ self.check_extra_permissions(request)
+ self.check_method_extra_permissions(request)
+
+ def get_view_loggers(self, request: Request, *args, **kwargs):
+ logger_classes = self.view_logger_classes or []
+ if not logger_classes:
+ return []
+ view_loggers = [logger_class(view=self, request=request, *args, **kwargs) for logger_class in logger_classes]
+ return view_loggers
+
+ def handle_logging(self, request: Request, *args, **kwargs):
+ view_loggers = self.get_view_loggers(request, *args, **kwargs)
+ method = request.method.lower()
+ for view_logger in view_loggers:
+ view_logger.handle(request, *args, **kwargs)
+ logger_fun = getattr(view_logger, f'handle_{method}', None)
+ if logger_fun and isinstance(logger_fun, (FunctionType, MethodType)):
+ logger_fun(request, *args, **kwargs)
+
+ def get_extra_permissions(self):
+ return [permission() for permission in self.extra_permission_classes]
+
+ def check_extra_permissions(self, request: Request):
+ for permission in self.get_extra_permissions():
+ if not permission.has_permission(request, self):
+ self.permission_denied(
+ request, message=getattr(permission, 'message', None)
+ )
+
+ def get_method_extra_permissions(self):
+ _name = self.request.method.upper()
+ method_extra_permission_classes = getattr(self, f"{_name}_permission_classes", None)
+ if not method_extra_permission_classes:
+ return []
+ return [permission() for permission in method_extra_permission_classes]
+
+ def check_method_extra_permissions(self, request):
+ for permission in self.get_method_extra_permissions():
+ if not permission.has_permission(request, self):
+ self.permission_denied(
+ request, message=getattr(permission, 'message', None)
+ )
+
+
+class BatchModelApIView(CustomAPIView):
+ """
+ 模型批量CRUD通用视图
+ """
+ model = None
+ serializer_class = None
+ POST_serializer_class = None
+ PUT_serializer_class = None
+ field_name = 'instanceId'
+ instanceId_list_param_name = 'instanceIdList'
+ instance_info_param_name = 'info'
+
+ def get_serializer(self, *args, **kwargs):
+ if not self.request:
+ return None
+ serializer_class = getattr(self, f"{self.request.method}_serializer_class", None) or getattr(self,
+ 'serializer_class')
+ serializer = serializer_class(*args, **kwargs)
+ if isinstance(serializer, CustomModelSerializer):
+ serializer.request = self.request
+ return serializer
+
+ def get(self, request: Request = None, *args, **kwargs):
+ data = self.get_serializer(self.model.objects.filter(**{f'{self.field_name}__in': request.data}),
+ many=True).data
+ return SuccessResponse(data=data)
+
+ def post(self, request: Request = None, *args, **kwargs):
+ data = []
+ for info in request.data:
+ serializer = self.get_serializer(data=info)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ data.append(serializer.data)
+ return SuccessResponse(data=data)
+
+ def put(self, request: Request = None, *args, **kwargs):
+ data = []
+ instanceId_list = request.data.get(self.instanceId_list_param_name, [])
+ info = request.data.get(self.instance_info_param_name, {})
+ for instanceId in instanceId_list:
+ serializer = self.get_serializer(
+ instance=self.model.objects.get(**{f'{self.field_name}': instanceId}),
+ data=info,
+ partial=True
+ )
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return SuccessResponse(data=instanceId_list)
+
+ def delete(self, request: Request = None, *args, **kwargs):
+ self.model.objects.filter(**{f'{self.field_name}__in': request.data}).delete()
+ return SuccessResponse(data=request.data)
+
+
+class ModelRelationshipAPIView(CustomAPIView):
+ """
+ 模型关联关系通用CRUD视图
+ """
+ model = None
+ through_model = None
+ relationship_model = None
+
+ relationship_serializer = None
+ field_name: str = None
+ from_field_name: str = 'instanceId'
+ to_field_name: str = None
+ relationship_field_values = ()
+
+ view_logger_classes = [CustomerRelationshipViewLogger, ]
+
+ def get_relationship_data(self, instanceId: str):
+ relationship_model_field_name = self.relationship_field_values[0]
+ params = {}
+ params[self.field_name] = instanceId
+ business_key_dict = self.through_model.objects.filter(**params).values(
+ *self.relationship_field_values).distinct()
+ business_key_list = [ele[relationship_model_field_name] for ele in business_key_dict]
+
+ params = {}
+ params[f"{self.to_field_name}__in"] = business_key_list
+ queryset = self.relationship_model.objects.filter(**params)
+
+ data = ModelRelateUtils.model_to_dict(queryset, self.relationship_serializer, default=[])
+ if 'creator' in self.relationship_field_values and 'ctime' in self.relationship_field_values:
+ for _index in range(len(data)):
+ ele = data[_index]
+ ele['relationship_creator'] = business_key_dict[_index]['creator']
+ ele['relationship_ctime'] = business_key_dict[_index]['ctime']
+ return data
+
+ def execute_method(self, execute: str, request: Request, instanceId: str, *args, **kwargs):
+ method = request.method.lower()
+ fun = None
+ if execute == 'before':
+ fun = getattr(self, f'before_{method}', None)
+ elif execute == 'handle':
+ fun = getattr(self, f'handle_{method}', None)
+ elif execute == 'after':
+ fun = getattr(self, f'after_{method}', None)
+ if fun and isinstance(fun, (FunctionType, MethodType)):
+ fun(request, instanceId, *args, **kwargs)
+
+ def do_request(self, request: Request, instanceId: str, *args, **kwargs):
+ self.execute_method('before', request, instanceId, *args, **kwargs)
+ self.execute_method('handle', request, instanceId, *args, **kwargs)
+ self.execute_method('after', request, instanceId, *args, **kwargs)
+ self.handle_logging(request, instanceId=instanceId, *args, **kwargs)
+ data = self.get_relationship_data(instanceId)
+ return SuccessResponse(data)
+
+ def get(self, request: Request, instanceId: str, *args, **kwargs):
+ return self.do_request(request, instanceId, *args, **kwargs)
+
+ def post(self, request: Request, instanceId: str, *args, **kwargs):
+ return self.do_request(request, instanceId, *args, **kwargs)
+
+ def put(self, request: Request, instanceId: str, *args, **kwargs):
+ return self.do_request(request, instanceId, *args, **kwargs)
+
+ def delete(self, request: Request, instanceId: str, *args, **kwargs):
+ return self.do_request(request, instanceId, *args, **kwargs)
+
+
+class ModelRelationshipView(ModelRelationshipAPIView):
+ """
+ 模型关联关系通用CRUD视图
+ """
+
+ def handle_get(self, request: Request, instanceId: str, *args, **kwargs):
+ data = self.get_relationship_data(instanceId)
+ return SuccessResponse(data)
+
+ def handle_post(self, request: Request, instanceId: str, *args, **kwargs):
+ relationship_model_field_name = self.relationship_field_values[0]
+ params = {}
+ params[f"{self.to_field_name}__in"] = request.data
+ queryset = self.relationship_model.objects.filter(**params)
+
+ exist_list = [getattr(ele, self.to_field_name) for ele in queryset]
+ bulk_info = []
+ for _id in exist_list:
+ info = {}
+ info[relationship_model_field_name] = _id
+ info[self.field_name] = instanceId
+ info['creator'] = request.user.username
+ bulk_info.append(self.through_model(**info))
+ self.through_model.objects.bulk_create(bulk_info)
+ data = self.get_relationship_data(instanceId)
+ return SuccessResponse(data)
+
+ def handle_put(self, request: Request, instanceId: str, *args, **kwargs):
+ relationship_model_field_name = self.relationship_field_values[0]
+
+ params1 = {}
+ params1[f"{self.field_name}"] = instanceId
+ params2 = {}
+ params2[f"{relationship_model_field_name}__in"] = request.data
+
+ relationships = self.through_model.objects.filter(**params1).exclude(**params2)
+ relationships.delete()
+
+ params = {}
+ params[f"{self.field_name}"] = instanceId
+
+ instanceId_dict = self.through_model.objects.filter(**params).values(*self.relationship_field_values).distinct()
+ instanceId_list = [ele.get(relationship_model_field_name) for ele in instanceId_dict]
+ create_list = list(set(request.data).difference(set(instanceId_list)))
+ for _id in create_list:
+ info = {}
+ info[relationship_model_field_name] = _id
+ info[self.field_name] = instanceId
+ info['creator'] = request.user.username
+ data = self.get_relationship_data(instanceId)
+ return SuccessResponse(data)
+
+ def handle_delete(self, request: Request, instanceId: str, *args, **kwargs):
+ relationship_model_field_name = self.relationship_field_values[0]
+ params = {}
+ params[f"{self.field_name}"] = instanceId
+ params[f"{relationship_model_field_name}__in"] = request.data
+ self.through_model.objects.filter(**params).delete()
+ data = self.get_relationship_data(instanceId)
+ return SuccessResponse(data)
diff --git a/dvadmin-backend/apps/op_drf/viewsets.py b/dvadmin-backend/apps/op_drf/viewsets.py
new file mode 100644
index 0000000..739a0b9
--- /dev/null
+++ b/dvadmin-backend/apps/op_drf/viewsets.py
@@ -0,0 +1,249 @@
+from types import FunctionType, MethodType
+
+# from rest_framework_mongoengine.generics import GenericAPIView as MongoGenericAPIView
+from django.core.exceptions import ValidationError
+from django.http.response import Http404
+from django.shortcuts import get_object_or_404 as _get_object_or_404
+from django_filters.rest_framework import DjangoFilterBackend
+from mongoengine.queryset.base import BaseQuerySet
+from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework.request import Request
+from rest_framework.settings import api_settings
+from rest_framework.viewsets import ViewSetMixin
+
+from utils.exceptions import APIException
+from . import mixins
+from .filters import MongoSearchFilter, MongoOrderingFilter, AdvancedSearchFilter, MongoAdvancedSearchFilter
+from .generics import GenericAPIView
+from .logging.view_logger import CustomerModelViewLogger
+from .pagination import Pagination
+from .serializers import CustomModelSerializer
+
+
+def get_object_or_404(queryset, *filter_args, **filter_kwargs):
+ try:
+ return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
+ except (TypeError, ValueError, ValidationError, Http404):
+ raise APIException(message='该对象不存在或者无访问权限')
+
+
+class GenericViewSet(ViewSetMixin, GenericAPIView):
+ extra_filter_backends = []
+ pagination_class = Pagination
+ filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter, AdvancedSearchFilter]
+ view_logger_classes = (CustomerModelViewLogger,)
+
+ def handle_logging(self, request: Request, *args, **kwargs):
+ view_loggers = self.get_view_loggers(request, *args, **kwargs)
+ for view_logger in view_loggers:
+ handle_action = getattr(view_logger, f'handle_{self.action}', None)
+ if handle_action and isinstance(handle_action, (FunctionType, MethodType)):
+ handle_action(request, *args, **kwargs)
+
+ def get_serializer(self, *args, **kwargs):
+ serializer_class = self.get_serializer_class()
+ kwargs['context'] = self.get_serializer_context()
+ serializer = serializer_class(*args, **kwargs)
+ if isinstance(serializer, CustomModelSerializer):
+ serializer.request = self.request
+ return serializer
+
+ def filter_queryset(self, queryset):
+ for backend in set(set(self.filter_backends) | set(self.extra_filter_backends or [])):
+ queryset = backend().filter_queryset(self.request, queryset, self)
+ queryset = self.action_extra_filter_queryset(queryset)
+ return queryset
+
+ def action_extra_filter_queryset(self, queryset):
+ action__extra_filter_backends = getattr(self, f"{self.action}_extra_filter_backends", None)
+ if not action__extra_filter_backends:
+ return queryset
+ for backend in action__extra_filter_backends:
+ queryset = backend().filter_queryset(self.request, queryset, self)
+ return queryset
+
+ def get_serializer_class(self):
+ action_serializer_name = f"{self.action}_serializer_class"
+ action_serializer_class = getattr(self, action_serializer_name, None)
+ if action_serializer_class:
+ return action_serializer_class
+ return super().get_serializer_class()
+
+ def reverse_action(self, url_name, *args, **kwargs):
+ return super().reverse_action(url_name, *args, **kwargs)
+
+ def get_action_extra_permissions(self):
+ """
+ 获取已配置的action权限校验,并且实例化其对象
+ :return:
+ """
+ action_extra_permission_classes = getattr(self, f"{self.action}_extra_permission_classes", None)
+ if not action_extra_permission_classes:
+ return []
+ return [permission() for permission in action_extra_permission_classes]
+
+ def check_action_extra_permissions(self, request):
+ """
+ 逐个校验action权限校验
+ :param request:
+ :return:
+ """
+ for permission in self.get_action_extra_permissions():
+ if not permission.has_permission(request, self):
+ self.permission_denied(
+ request, message=getattr(permission, 'message', None)
+ )
+
+ def check_action_extra_object_permissions(self, request, obj):
+ """
+ action方法的专属对象权限校验
+ :param request:
+ :param obj:
+ :return:
+ """
+ for permission in self.get_action_extra_permissions():
+ if not permission.has_object_permission(request, self, obj):
+ self.permission_denied(
+ request, message=getattr(permission, 'message', None)
+ )
+
+ def initial(self, request, *args, **kwargs):
+ """
+ 重写initial方法
+ (1)新增action的权限校验
+ :param request:
+ :param args:
+ :param kwargs:
+ :return:
+ """
+ super().initial(request, *args, **kwargs)
+ self.check_action_extra_permissions(request)
+
+ def get_object(self):
+ queryset = self.filter_queryset(self.get_queryset())
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ assert lookup_url_kwarg in self.kwargs, (
+ 'Expected view %s to be called with a URL keyword argument '
+ 'named "%s". Fix your URL conf, or set the `.lookup_field` '
+ 'attribute on the view correctly.' %
+ (self.__class__.__name__, lookup_url_kwarg)
+ )
+ filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
+ obj = get_object_or_404(queryset, **filter_kwargs)
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+ def check_object_permissions(self, request, obj):
+ """
+ 重新check_object_permissions
+ (1)新增action方法的专属对象权限检查入口
+ (2)先校验共同的object_permissions, 再校验action的object_permissions
+ :param request:
+ :param obj:
+ :return:
+ """
+ super().check_object_permissions(request, obj)
+ self.check_action_extra_object_permissions(request, obj)
+
+
+class MongoGenericAPIView(GenericAPIView):
+ """ Adaptation of DRF GenericAPIView """
+ lookup_field = 'id'
+
+ def get_queryset(self):
+ queryset = super(MongoGenericAPIView, self).get_queryset()
+ if isinstance(queryset, BaseQuerySet):
+ queryset = queryset.all()
+ return queryset
+
+ def get_object(self):
+ queryset = self.filter_queryset(self.get_queryset())
+ # Perform the lookup filtering.
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ assert lookup_url_kwarg in self.kwargs, (
+ 'Expected view %s to be called with a URL keyword argument '
+ 'named "%s". Fix your URL conf, or set the `.lookup_field` '
+ 'attribute on the view correctly.' %
+ (self.__class__.__name__, lookup_url_kwarg)
+ )
+ filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
+ obj = get_object_or_404(queryset, **filter_kwargs)
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+
+class MongoGenericViewSet(ViewSetMixin, MongoGenericAPIView):
+ pagination_class = Pagination
+ pass
+
+
+class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
+ mixins.ListModelMixin,
+ GenericViewSet):
+ pass
+
+
+class ModelViewSet(mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ mixins.ListModelMixin,
+ GenericViewSet):
+ pass
+
+
+class MongoModelViewSet(mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ mixins.ListModelMixin,
+ MongoGenericViewSet):
+ pass
+
+
+class CustomModelViewSet(ModelViewSet, mixins.TableSerializerMixin):
+ """
+ 自定义的ModelViewSet:
+ (1)默认分页器就为统一分页器op_drf.pagination.Pagination
+ (1)默认使用统一标准返回格式
+ (1)默认支持高级搜索
+ (1)默认支持生成前端动态table的option
+ (1)ORM性能优化, 尽可能使用values_queryset形式
+ """
+ values_queryset = None
+ ordering_fields = '__all__'
+
+ def get_queryset(self):
+ if getattr(self, 'values_queryset', None):
+ return self.values_queryset
+ return super().get_queryset()
+
+
+class CustomMongoModelViewSet(MongoModelViewSet, mixins.TableSerializerMixin):
+ filter_backends = (MongoOrderingFilter, MongoSearchFilter, MongoAdvancedSearchFilter)
+ # filter_fields = '__all__' # 暂不支持__all__
+ filter_fields = ()
+ search_fields = ()
+ ordering_fields = '__all__'
+ view_logger_classes = (CustomerModelViewLogger,)
+
+ def get_queryset(self):
+ queryset = self.queryset
+ filtering_kwargs = {}
+ for param in self.request.query_params:
+ param = param.strip()
+ if param in ['page_size', 'page', 'search', 'ordering', 'as']: continue
+ if self.filter_fields == '__all__' or param in self.filter_fields:
+ # if param in self.filter_fields:
+ filtering_kwargs[param] = self.request.query_params[param]
+ queryset = queryset.filter(**filtering_kwargs)
+ ordering_params = self.request.query_params.get(api_settings.ORDERING_PARAM, None)
+ if ordering_params:
+ ordering_fields = [field.strip() for field in ordering_params.split(',')]
+ ordering_fields = filter(lambda field: self.ordering_fields == '__all__' or field in self.ordering_fields,
+ ordering_fields)
+ queryset = queryset.order_by(*ordering_fields)
+ return queryset
+
+ def filter_queryset(self, queryset):
+ return super().filter_queryset(queryset)
diff --git a/dvadmin-backend/utils/exceptions.py b/dvadmin-backend/utils/exceptions.py
index e6bd249..86ef46e 100644
--- a/dvadmin-backend/utils/exceptions.py
+++ b/dvadmin-backend/utils/exceptions.py
@@ -1,7 +1,7 @@
import logging
import traceback
-from utils.response import ErrorResponse
+from .response import ErrorResponse
logger = logging.getLogger(__name__)
diff --git a/dvadmin-backend/utils/jsonpath_util.py b/dvadmin-backend/utils/jsonpath_util.py
new file mode 100644
index 0000000..bf5451d
--- /dev/null
+++ b/dvadmin-backend/utils/jsonpath_util.py
@@ -0,0 +1,47 @@
+import logging
+from collections import Iterable
+
+import jsonpath
+
+logger = logging.getLogger(__name__)
+
+
+def get_jsonpath(params: dict = None, exclude_params: list = None, type_params: dict = None):
+ if params is None:
+ params = {}
+ if exclude_params is None:
+ exclude_params = []
+ _filters = []
+ for param_name, param_value in params.items():
+ if param_name in exclude_params:
+ continue
+ if type_params is None or type_params.get(param_name, 'str') == 'str':
+ _filter = f"@.{param_name}=='{param_value}'"
+ else:
+ _filter = f"@.{param_name}=={param_value}"
+ _filters.append(_filter)
+ _path = " || ".join(_filters)
+ if not _path:
+ return ""
+ return f"[?({_path})]"
+
+
+def filter_json(obj: list, expr: str, *args, **kwargs):
+ if not isinstance(obj, Iterable):
+ return []
+ if not expr.startswith('$'):
+ expr = f"${expr}"
+ logger.debug(f"expr={expr}, len={len(obj)}")
+ return jsonpath.jsonpath(obj, expr, *args)
+
+
+def search_json(obj: list, search: str, search_fields: list):
+ queryset = []
+ search = search.lower()
+ for ele in obj:
+ for field_name in search_fields:
+ value = ele.get(field_name, None)
+ if value and search in str(value).lower():
+ queryset.append(ele)
+ break
+ return queryset
diff --git a/dvadmin-backend/utils/model_util.py b/dvadmin-backend/utils/model_util.py
new file mode 100644
index 0000000..9c2921d
--- /dev/null
+++ b/dvadmin-backend/utils/model_util.py
@@ -0,0 +1,169 @@
+import json
+from collections import Iterable
+
+from django.apps import apps
+from django.apps.config import AppConfig
+from django.db.models.fields import Field
+from rest_framework.renderers import JSONRenderer
+
+
+def get_primary_field(model, many=False):
+ """
+ 获取模型的主键列对应的Field
+ :param model:
+ :param many:
+ :return:
+ """
+ primary_field: Field = list(filter(lambda field: field.primary_key, model._meta.local_fields))
+ if many:
+ return primary_field
+ return primary_field[0]
+
+
+def get_primary_key_name(model, many=False):
+ primary_field = get_primary_field(model=model, many=many)
+ if many:
+ return [field.name for field in primary_field]
+ return primary_field.name
+
+
+def get_business_key_name(model):
+ """
+ 获取业务列名称
+ :param model:
+ :return:
+ """
+ return getattr(model, 'business_field_name', get_primary_key_name(model, False))
+
+
+def get_business_field(model):
+ """
+ 获取模型的业务列对应的Field
+ :param model:
+ :return:
+ """
+ business_key_name = get_business_key_name(model)
+ business_field = list(filter(lambda field: field.name == business_key_name, model._meta.local_fields))
+ return business_field[0]
+
+
+def get_model(app_label: str = None, model_name: str = None, model_label: str = None):
+ """
+ 根据App、Model名称获取model_class
+ 使用:get_model(app_label='op_cmdb', model_name='Business')
+ 或者:get_model(model_label='op_cmdb.Business')
+ :param app_label: settings中注册的app的名称, 例如:op_cmdb, admin
+ :param model_name: 某个app中模型的类名, 如:Business, host, dept(忽略大小写)
+ :param model_label: 例如: op_cmdb.Business
+ :return:
+ """
+ if model_label:
+ app_label, model_name = model_label.split(".")
+ app_conf: AppConfig = apps.get_app_config(app_label)
+ return app_conf.get_model(model_name)
+
+
+class ModelRelateUtils:
+ """
+ 封装ORM模型的映射操作,例如
+
+ """
+
+ @classmethod
+ def model_to_dict(cls, models=None, serializer=None, default=None):
+ """
+ ORM模型对象转化为字典
+ :param models: 模型对象
+ :param serializer: 模型的序列化器
+ :param default:
+ :return:
+ """
+ if default is None:
+ default = {}
+ if not models or not serializer:
+ return default
+ is_iterable = isinstance(models, Iterable) and not isinstance(models, dict)
+ if is_iterable:
+ return [json.loads(JSONRenderer().render(serializer(model).data)) for model in models]
+ return json.loads(JSONRenderer().render(serializer(models).data))
+
+ @classmethod
+ def serializer_to_dict(cls, datas):
+ """
+ ORM模型对象转化为字典
+ :param datas: 序列化器反序列化之后的data
+ :return:
+ """
+ is_iterable = isinstance(datas, Iterable)
+ if is_iterable:
+ return [json.loads(JSONRenderer().render(data)) for data in datas]
+ return json.loads(JSONRenderer().render(datas))
+
+ @classmethod
+ def executeModelRelate(cls, model, related_name, fun_name, id_list):
+ """
+ 执行RelatedManager的add方法
+ :param model: Model
+ :param related_name: 映射名称
+ :param fun_name: 函数名称
+ :param id_list: 单个或者多个
+ :return:
+ """
+ # 获取函数
+ related_manager = getattr(model, related_name, '')
+ if not related_manager:
+ return 0
+ return cls.executeRelatedManager(related_manager, fun_name, id_list)
+
+ @classmethod
+ def executeRelatedManager(cls, related_manager, fun_name, id_list):
+ """
+ 执行RelatedManager的add方法
+ :param related_manager: RelatedManager
+ :param fun_name: RelatedManager的函数名称
+ :param id_list: 单个或者多个
+ :return:
+ """
+ # 获取函数
+ fun = getattr(related_manager, fun_name, '')
+ # 判断是一个函数
+ if not hasattr(fun, "__call__"):
+ return 0
+ # 判断参数是否一个集合
+ is_iterable = isinstance(id_list, Iterable) and type(id_list) != str
+ if is_iterable:
+ fun(*id_list)
+ return len(id_list)
+ else:
+ fun(id_list)
+ return 1
+
+ @classmethod
+ def executeRelatedManagerAddMethod(cls, related_manager, id_list):
+ """
+ 执行RelatedManager的add方法
+ :param related_manager: RelatedManager
+ :param id_list: 单个或者多个
+ :return:
+ """
+ return cls.executeRelatedManager(related_manager, 'add', id_list)
+
+ @classmethod
+ def executeRelatedManagerSetMethod(cls, related_manager, id_list):
+ """
+ 执行RelatedManager的add方法
+ :param related_manager: RelatedManager
+ :param id_list: 单个或者多个
+ :return:
+ """
+ return cls.executeRelatedManager(related_manager, 'set', id_list)
+
+ @classmethod
+ def executeRelatedManagerRemoveMethod(cls, related_manager, id_list):
+ """
+ 执行RelatedManager的remove方法
+ :param related_manager: RelatedManager
+ :param id_list: 单个或者多个
+ :return:
+ """
+ return cls.executeRelatedManager(related_manager, 'remove', id_list)
diff --git a/dvadmin-backend/utils/request_util.py b/dvadmin-backend/utils/request_util.py
new file mode 100644
index 0000000..47b3b8e
--- /dev/null
+++ b/dvadmin-backend/utils/request_util.py
@@ -0,0 +1,128 @@
+"""
+Request工具类
+"""
+import json
+import logging
+from django.contrib.auth.models import AbstractBaseUser
+from rest_framework.authentication import BaseAuthentication
+from rest_framework.settings import api_settings as drf_settings
+from django.contrib.auth.models import AnonymousUser
+from django.urls.resolvers import ResolverMatch
+
+
+logger = logging.getLogger(__name__)
+
+
+def get_request_user(request, authenticate=True):
+ """
+ 获取请求user
+ (1)如果request里的user没有认证,那么则手动认证一次
+ :param request:
+ :param authenticate:
+ :return:
+ """
+ user: AbstractBaseUser = getattr(request, 'user', None)
+ if user and user.is_authenticated:
+ return user
+ authentication: BaseAuthentication = None
+ for authentication_class in drf_settings.DEFAULT_AUTHENTICATION_CLASSES:
+ try:
+ authentication = authentication_class()
+ user_auth_tuple = authentication.authenticate(request)
+ if user_auth_tuple is not None:
+ user, token = user_auth_tuple
+ if authenticate:
+ request.user = user
+ return user
+ except Exception:
+ pass
+ return user or AnonymousUser()
+
+
+def get_request_ip(request):
+ """
+ 获取请求IP
+ :param request:
+ :return:
+ """
+ ip = getattr(request, 'request_ip', None)
+ if ip:
+ return ip
+ ip = request.META.get('REMOTE_ADDR', '')
+ if not ip:
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[-1].strip()
+ else:
+ ip = 'unknown'
+ return ip
+
+
+def get_request_data(request):
+ """
+ 获取请求参数
+ :param request:
+ :return:
+ """
+ request_data = getattr(request, 'request_data', None)
+ if request_data:
+ return request_data
+ data: dict = {**request.GET.dict(), **request.POST.dict()}
+ if not data:
+ body = getattr(request, '_body', request.body)
+ if body:
+ data = json.loads(body)
+ if not isinstance(data, dict):
+ data = {'data': data}
+ return data
+
+
+def get_request_path(request, *args, **kwargs):
+ """
+ 获取请求路径
+ :param request:
+ :param args:
+ :param kwargs:
+ :return:
+ """
+ request_path = getattr(request, 'request_path', None)
+ if request_path:
+ return request_path
+ values = []
+ for arg in args:
+ if len(arg) == 0:
+ continue
+ if isinstance(arg, str):
+ values.append(arg)
+ elif isinstance(arg, (tuple, set, list)):
+ values.extend(arg)
+ elif isinstance(arg, dict):
+ values.extend(arg.values())
+ if len(values) == 0:
+ return request.path
+ path: str = request.path
+ for value in values:
+ path = path.replace('/' + value, '/' + '{id}')
+ return path
+
+
+def get_request_canonical_path(request, *args, **kwargs):
+ """
+ 获取请求路径
+ :param request:
+ :param args:
+ :param kwargs:
+ :return:
+ """
+ request_path = getattr(request, 'request_canonical_path', None)
+ if request_path:
+ return request_path
+ path: str = request.path
+ resolver_match: ResolverMatch = request.resolver_match
+ for value in resolver_match.args:
+ path = path.replace(f"/{value}", "/{id}")
+ for key, value in resolver_match.kwargs.items():
+ path = path.replace(f"/{value}", f"/{{{key}}}")
+ if key == 'pk':
+ pass
+ return path
diff --git a/dvadmin-backend/utils/sort_util.py b/dvadmin-backend/utils/sort_util.py
new file mode 100644
index 0000000..b894b3f
--- /dev/null
+++ b/dvadmin-backend/utils/sort_util.py
@@ -0,0 +1,80 @@
+"""
+封装排序:
+ ● 普通类型列表的排序
+ ● 字典列表的排序
+ ● 对象列表的排序
+"""
+import operator
+from collections import Iterable
+
+
+def sortSimpleTypeList(li, reverse=False):
+ """
+ 排序简单的类型
+ :param li:
+ :param reverse:
+ :return:
+ """
+ li.sort()
+ if reverse:
+ li.reverse()
+ return li
+
+
+def sortObjectList(obj_list, by_prop):
+ """
+ 排序列表:按照对象的某个属性排序
+ :param obj_list:
+ :param by_prop:
+ :return:
+ """
+ reverse = False
+ if by_prop.startswith('-'):
+ reverse = True
+ by_prop = by_prop[1:]
+ fun = operator.attrgetter(by_prop)
+ obj_list.sort(key=fun)
+ if reverse:
+ obj_list.reverse()
+ return obj_list
+
+
+def sortDictList(dict_list, by_key):
+ """
+ 排序字典列表:按照字典的某个key的value排序
+ :param dict_list:
+ :param by_key:
+ :return:
+ """
+ reverse = False
+ if by_key.startswith('-'):
+ reverse = True
+ by_key = by_key[1:]
+ dict_list.sort(key=lambda ele: ele[by_key])
+ if reverse:
+ dict_list.reverse()
+ return dict_list
+
+
+def sortList(li, by=''):
+ """
+ 排序集合: by加上前缀'-'表示逆序
+ :param li: 字典集合 or 对象集合
+ :param by: 通过哪一个属性, name 按照name排序; -name 按照name反向排序
+ :return: 原对象(排序后集合, 不是返回新集合)
+ """
+ reverse = False
+ if not li or not isinstance(li, Iterable):
+ # 不是集合, 或空集合返回原内容
+ return li
+ if by.startswith('-'):
+ reverse = True
+ if isinstance(li[0], (int, float, str)):
+ return sortSimpleTypeList(li, reverse)
+ if not by:
+ # 非简单类型的list, 必须要传入by, 否则不排序
+ return li
+ if isinstance(li[0], dict):
+ # 如果第一个元素是字典类型,则默认所有元素都是字典类型
+ return sortDictList(li, by)
+ return sortObjectList(li, by)
diff --git a/dvadmin-backend/utils/string_util.py b/dvadmin-backend/utils/string_util.py
index ff4d9da..c05c3a2 100644
--- a/dvadmin-backend/utils/string_util.py
+++ b/dvadmin-backend/utils/string_util.py
@@ -1,6 +1,5 @@
"""
封装字符串相关函数:UUID字符串,字符串加密解密
-@author: wanglei
"""
import uuid as UUID
import base64
@@ -108,4 +107,4 @@ def encode_text(text, crypto=""):
text = bas64_encode_text(text)
else:
text = text
- return text
\ No newline at end of file
+ return text