perf: Export resources to add operation logs

This commit is contained in:
jiangweidong 2025-04-03 14:11:45 +08:00 committed by Bryan
parent e8e0ea920b
commit 1f60e328b6
7 changed files with 105 additions and 91 deletions

View File

@ -1,65 +1,15 @@
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from django.db.models import Model
from django.utils import translation from django.utils import translation
from django.utils.translation import gettext_noop
from audits.const import ActionChoices from audits.const import ActionChoices
from common.views.mixins import RecordViewLogMixin from audits.handler import create_or_update_operate_log
from common.utils import i18n_fmt
class AccountRecordViewLogMixin(RecordViewLogMixin): class AccountRecordViewLogMixin(object):
get_object: callable get_object: callable
get_queryset: callable model: Model
@staticmethod
def _filter_params(params):
new_params = {}
need_pop_params = ('format', 'order')
for key, value in params.items():
if key in need_pop_params:
continue
if isinstance(value, list):
value = list(filter(None, value))
if value:
new_params[key] = value
return new_params
def get_resource_display(self, request):
query_params = dict(request.query_params)
params = self._filter_params(query_params)
spm_filter = params.pop("spm", None)
if not params and not spm_filter:
display_message = gettext_noop("Export all")
elif spm_filter:
display_message = gettext_noop("Export only selected items")
else:
query = ",".join(
["%s=%s" % (key, value) for key, value in params.items()]
)
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
return display_message
@property
def detail_msg(self):
return i18n_fmt(
gettext_noop('User %s view/export secret'), self.request.user
)
def list(self, request, *args, **kwargs):
list_func = getattr(super(), 'list')
if not callable(list_func):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
response = list_func(request, *args, **kwargs)
with translation.override('en'):
resource_display = self.get_resource_display(request)
ids = [q.id for q in self.get_queryset()]
self.record_logs(
ids, ActionChoices.view, self.detail_msg, resource_display=resource_display
)
return response
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
retrieve_func = getattr(super(), 'retrieve') retrieve_func = getattr(super(), 'retrieve')
@ -67,9 +17,9 @@ class AccountRecordViewLogMixin(RecordViewLogMixin):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
response = retrieve_func(request, *args, **kwargs) response = retrieve_func(request, *args, **kwargs)
with translation.override('en'): with translation.override('en'):
resource = self.get_object() create_or_update_operate_log(
self.record_logs( ActionChoices.view, self.model._meta.verbose_name,
[resource.id], ActionChoices.view, self.detail_msg, resource=resource force=True, resource=self.get_object(),
) )
return response return response

View File

@ -24,6 +24,7 @@ class ActionChoices(TextChoices):
update = "update", _("Update") update = "update", _("Update")
delete = "delete", _("Delete") delete = "delete", _("Delete")
create = "create", _("Create") create = "create", _("Create")
export = "export", _("Export")
# Activities action # Activities action
download = "download", _("Download") download = "download", _("Download")
connect = "connect", _("Connect") connect = "connect", _("Connect")

View File

@ -6,12 +6,16 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.db.models import F, Value, CharField from django.db.models import F, Value, CharField
from django.db.models.functions import Concat from django.db.models.functions import Concat
from django.utils import translation
from itertools import chain from itertools import chain
from common.db.fields import RelatedManager from common.db.fields import RelatedManager
from common.utils import validate_ip, get_ip_city, get_logger from common.utils import validate_ip, get_ip_city, get_logger
from common.utils.timezone import as_current_tz from common.utils.timezone import as_current_tz
from .const import DEFAULT_CITY from .const import DEFAULT_CITY, ActivityChoices as LogChoice
from .handler import create_or_update_operate_log
from .models import ActivityLog
logger = get_logger(__name__) logger = get_logger(__name__)
@ -140,3 +144,15 @@ def construct_userlogin_usernames(user_queryset):
).values_list("usernames_combined_field", flat=True) ).values_list("usernames_combined_field", flat=True)
usernames = list(chain(usernames_original, usernames_combined)) usernames = list(chain(usernames_original, usernames_combined))
return usernames return usernames
def record_operate_log_and_activity_log(ids, action, detail, model, **kwargs):
from orgs.utils import current_org
org_id = current_org.id
with translation.override('en'):
resource_type = model._meta.verbose_name
create_or_update_operate_log(action, resource_type, force=True, **kwargs)
base_data = {'type': LogChoice.operate_log, 'detail': detail, 'org_id': org_id}
activities = [ActivityLog(resource_id=r_id, **base_data) for r_id in ids]
ActivityLog.objects.bulk_create(activities)

View File

@ -13,11 +13,13 @@ from rest_framework.utils import encoders, json
from common.serializers import fields as common_fields from common.serializers import fields as common_fields
from common.utils import get_logger from common.utils import get_logger
from .mixins import LogMixin
logger = get_logger(__file__) logger = get_logger(__file__)
class BaseFileRenderer(BaseRenderer): class BaseFileRenderer(LogMixin, BaseRenderer):
# 渲染模板标识, 导入、导出、更新模板: ['import', 'update', 'export'] # 渲染模板标识, 导入、导出、更新模板: ['import', 'update', 'export']
template = 'export' template = 'export'
serializer = None serializer = None
@ -256,6 +258,8 @@ class BaseFileRenderer(BaseRenderer):
logger.debug(e, exc_info=True) logger.debug(e, exc_info=True)
value = 'Render error! ({})'.format(self.media_type).encode('utf-8') value = 'Render error! ({})'.format(self.media_type).encode('utf-8')
return value return value
self.record_logs(request, view, data)
return value return value
def compress_into_zip_file(self, value, request, response): def compress_into_zip_file(self, value, request, response):

View File

@ -0,0 +1,69 @@
from django.utils.translation import gettext_noop
from audits.const import ActionChoices
from audits.utils import record_operate_log_and_activity_log
from common.utils import get_logger
logger = get_logger(__file__)
class LogMixin(object):
@staticmethod
def _clean_params(query_params):
clean_params = {}
ignore_params = ('format', 'order')
for key, value in dict(query_params).items():
if key in ignore_params:
continue
if isinstance(value, list):
value = list(filter(None, value))
if value:
clean_params[key] = value
return clean_params
@staticmethod
def _get_model(view):
model = getattr(view, 'model', None)
if not model:
serializer = view.get_serializer()
if serializer:
model = serializer.Meta.model
return model
@staticmethod
def _build_after(params, data):
base = {
gettext_noop('Resource count'): {'value': len(data)}
}
extra = {key: {'value': value} for key, value in params.items()}
return {**extra, **base}
@staticmethod
def get_resource_display(params):
spm_filter = params.pop("spm", None)
if not params and not spm_filter:
display_message = gettext_noop("Export all")
elif spm_filter:
display_message = gettext_noop("Export only selected items")
else:
display_message = gettext_noop("Export filtered")
return display_message
def record_logs(self, request, view, data):
activity_ids, activity_detail = [], ''
model = self._get_model(view)
if not model:
logger.warning('Model is not defined in view: %s' % view)
return
params = self._clean_params(request.query_params)
resource_display = self.get_resource_display(params)
after = self._build_after(params, data)
if hasattr(view, 'get_activity_detail_msg'):
activity_detail = view.get_activity_detail_msg()
activity_ids = [d['id'] for d in data if 'id' in d]
record_operate_log_and_activity_log(
activity_ids, ActionChoices.export, activity_detail,
model, resource_display=resource_display, after=after
)

View File

@ -2,20 +2,14 @@
# #
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.http.response import JsonResponse from django.http.response import JsonResponse
from django.db.models import Model
from django.utils import translation
from rest_framework import permissions from rest_framework import permissions
from rest_framework.request import Request from rest_framework.request import Request
from audits.const import ActivityChoices
from audits.handler import create_or_update_operate_log
from audits.models import ActivityLog
from common.exceptions import UserConfirmRequired from common.exceptions import UserConfirmRequired
from orgs.utils import current_org
__all__ = [ __all__ = [
"PermissionsMixin", "PermissionsMixin",
"RecordViewLogMixin",
"UserConfirmRequiredExceptionMixin", "UserConfirmRequiredExceptionMixin",
] ]
@ -45,23 +39,3 @@ class PermissionsMixin(UserPassesTestMixin):
if not permission_class().has_permission(self.request, self): if not permission_class().has_permission(self.request, self):
return False return False
return True return True
class RecordViewLogMixin:
model: Model
def record_logs(self, ids, action, detail, model=None, **kwargs):
with translation.override('en'):
model = model or self.model
resource_type = model._meta.verbose_name
create_or_update_operate_log(
action, resource_type, force=True, **kwargs
)
activities = [
ActivityLog(
resource_id=resource_id, type=ActivityChoices.operate_log,
detail=detail, org_id=current_org.id,
)
for resource_id in ids
]
ActivityLog.objects.bulk_create(activities)

View File

@ -18,6 +18,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from audits.const import ActionChoices from audits.const import ActionChoices
from audits.utils import record_operate_log_and_activity_log
from common.api import AsyncApiMixin from common.api import AsyncApiMixin
from common.const.http import GET, POST from common.const.http import GET, POST
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
@ -27,7 +28,6 @@ from common.permissions import IsServiceAccount
from common.storage.replay import ReplayStorageHandler, SessionPartReplayStorageHandler from common.storage.replay import ReplayStorageHandler, SessionPartReplayStorageHandler
from common.utils import data_to_json, is_uuid, i18n_fmt from common.utils import data_to_json, is_uuid, i18n_fmt
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.views.mixins import RecordViewLogMixin
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
@ -77,7 +77,7 @@ class SessionFilterSet(BaseFilterSet):
return queryset.filter(terminal__name=value) return queryset.filter(terminal__name=value)
class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): class SessionViewSet(OrgBulkModelViewSet):
model = Session model = Session
serializer_classes = { serializer_classes = {
'default': serializers.SessionSerializer, 'default': serializers.SessionSerializer,
@ -153,7 +153,7 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet):
detail = i18n_fmt( detail = i18n_fmt(
REPLAY_OP, self.request.user, _('Download'), str(session) REPLAY_OP, self.request.user, _('Download'), str(session)
) )
self.record_logs( record_operate_log_and_activity_log(
[session.asset_id], ActionChoices.download, detail, [session.asset_id], ActionChoices.download, detail,
model=Session, resource_display=str(session) model=Session, resource_display=str(session)
) )
@ -211,7 +211,7 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet):
return super().perform_create(serializer) return super().perform_create(serializer)
class SessionReplayViewSet(AsyncApiMixin, RecordViewLogMixin, viewsets.ViewSet): class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
serializer_class = serializers.ReplaySerializer serializer_class = serializers.ReplaySerializer
download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}" download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}"
session = None session = None
@ -283,7 +283,7 @@ class SessionReplayViewSet(AsyncApiMixin, RecordViewLogMixin, viewsets.ViewSet):
detail = i18n_fmt( detail = i18n_fmt(
REPLAY_OP, self.request.user, _('View'), str(session) REPLAY_OP, self.request.user, _('View'), str(session)
) )
self.record_logs( record_operate_log_and_activity_log(
[session.asset_id], ActionChoices.download, detail, [session.asset_id], ActionChoices.download, detail,
model=Session, resource_display=str(session) model=Session, resource_display=str(session)
) )