diff --git a/apps/assets/api/favorite_asset.py b/apps/assets/api/favorite_asset.py index 51658114b..0a333be82 100644 --- a/apps/assets/api/favorite_asset.py +++ b/apps/assets/api/favorite_asset.py @@ -4,16 +4,15 @@ from rest_framework_bulk.generics import BulkModelViewSet from common.permissions import IsValidUser from orgs.utils import tmp_to_root_org -from ..models import FavoriteAsset -from ..serializers import FavoriteAssetSerializer +from ..models import FavoriteAsset, FavoriteFolder +from ..serializers import FavoriteAssetSerializer, FavoriteFolderSerializer -__all__ = ['FavoriteAssetViewSet'] +__all__ = ['FavoriteAssetViewSet', 'FavoriteFolderViewSet'] -class FavoriteAssetViewSet(BulkModelViewSet): - serializer_class = FavoriteAssetSerializer +class FavoriteFolderViewSet(BulkModelViewSet): + serializer_class = FavoriteFolderSerializer permission_classes = (IsValidUser,) - filterset_fields = ['asset'] page_no_limit = True def dispatch(self, request, *args, **kwargs): @@ -21,7 +20,23 @@ class FavoriteAssetViewSet(BulkModelViewSet): return super().dispatch(request, *args, **kwargs) def get_queryset(self): - queryset = FavoriteAsset.objects.filter(user=self.request.user) + return FavoriteFolder.objects.filter(user=self.request.user) + + +class FavoriteAssetViewSet(BulkModelViewSet): + serializer_class = FavoriteAssetSerializer + permission_classes = (IsValidUser,) + filterset_fields = ['asset', 'folder'] + page_no_limit = True + + def dispatch(self, request, *args, **kwargs): + with tmp_to_root_org(): + return super().dispatch(request, *args, **kwargs) + + def get_queryset(self): + queryset = FavoriteAsset.objects.filter( + user=self.request.user + ).select_related('asset', 'asset__platform') return queryset def allow_bulk_destroy(self, qs, filtered): diff --git a/apps/assets/migrations/0020_favoritefolder.py b/apps/assets/migrations/0020_favoritefolder.py new file mode 100644 index 000000000..298699f98 --- /dev/null +++ b/apps/assets/migrations/0020_favoritefolder.py @@ -0,0 +1,44 @@ +# Generated for user custom favorite folders + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0019_alter_asset_connectivity'), + ] + + operations = [ + migrations.CreateModel( + name='FavoriteFolder', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='assets.favoritefolder')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Favorite folder', + 'unique_together': {('user', 'name', 'parent')}, + }, + ), + migrations.AddField( + model_name='favoriteasset', + name='folder', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.favoritefolder'), + ), + migrations.AlterUniqueTogether( + name='favoriteasset', + unique_together={('user', 'asset', 'folder')}, + ), + ] diff --git a/apps/assets/models/favorite_asset.py b/apps/assets/models/favorite_asset.py index 052551402..dad5e1602 100644 --- a/apps/assets/models/favorite_asset.py +++ b/apps/assets/models/favorite_asset.py @@ -5,15 +5,36 @@ from django.utils.translation import gettext_lazy as _ from common.db.models import JMSBaseModel -__all__ = ['FavoriteAsset'] +__all__ = ['FavoriteAsset', 'FavoriteFolder'] + + +class FavoriteFolder(JMSBaseModel): + """User custom favorite folder, owned by a user, visible across orgs, supports nesting""" + user = models.ForeignKey('users.User', on_delete=models.CASCADE) + name = models.CharField(max_length=128, verbose_name=_("Name")) + parent = models.ForeignKey( + 'self', on_delete=models.CASCADE, + null=True, blank=True, related_name='children' + ) + + class Meta: + unique_together = ('user', 'name', 'parent') + verbose_name = _("Favorite folder") + + def __str__(self): + return self.name class FavoriteAsset(JMSBaseModel): user = models.ForeignKey('users.User', on_delete=models.CASCADE) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE) + folder = models.ForeignKey( + 'assets.FavoriteFolder', on_delete=models.CASCADE, + null=True, blank=True, related_name='assets' + ) class Meta: - unique_together = ('user', 'asset') + unique_together = ('user', 'asset', 'folder') verbose_name = _("Favorite asset") @classmethod diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index 69c916f05..dfe1e948d 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,16 +4,57 @@ from rest_framework import serializers from common.serializers import BulkSerializerMixin -from ..models import FavoriteAsset +from ..models import FavoriteAsset, FavoriteFolder -__all__ = ['FavoriteAssetSerializer'] +__all__ = ['FavoriteAssetSerializer', 'FavoriteFolderSerializer'] + + +class FavoriteFolderSerializer(BulkSerializerMixin, serializers.ModelSerializer): + user = serializers.HiddenField( + default=serializers.CurrentUserDefault() + ) + + class Meta: + model = FavoriteFolder + fields = ['id', 'user', 'name', 'parent', 'date_created'] + read_only_fields = ['id', 'date_created'] class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): user = serializers.HiddenField( default=serializers.CurrentUserDefault() ) + asset_info = serializers.SerializerMethodField() class Meta: model = FavoriteAsset - fields = ['user', 'asset'] + fields = ['user', 'asset', 'folder', 'asset_info'] + + @staticmethod + def _get_icon(asset, platform): + from assets.const import AllTypes + support_types = AllTypes.get_types_values(exclude_custom=True) + if asset.category == 'device': + return 'switch' + if asset.type in support_types: + return asset.type + return 'file' + + def get_asset_info(self, obj): + asset = obj.asset + platform = asset.platform + return { + 'id': str(asset.id), + 'name': asset.name, + 'iconSkin': self._get_icon(asset, platform), + 'chkDisabled': not asset.is_active, + 'meta': { + 'type': 'asset', + 'data': { + 'platform_type': platform.type, + 'org_name': asset.org_name, + 'name': asset.name, + 'address': asset.address, + }, + }, + } diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 7527be88e..472f5f562 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -23,6 +23,7 @@ router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'zones', api.ZoneViewSet, 'zone') router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') +router.register(r'favorite-folders', api.FavoriteFolderViewSet, 'favorite-folder') router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting') router.register(r'labels', LabelViewSet, 'label') router.register(r'my-asset', api.MyAssetViewSet, 'my-asset') diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py index 6f875afb2..a07629dc4 100644 --- a/apps/audits/signal_handlers/operate_log.py +++ b/apps/audits/signal_handlers/operate_log.py @@ -80,18 +80,14 @@ def signal_of_operate_log_whether_continue( condition = False if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False): condition = False - # 不记录组件的操作日志 user = current_request.user if current_request else None if not user or getattr(user, 'is_service_account', False): condition = False - # 终端模型的 create 事件由系统产生,不记录 if instance._meta.object_name == 'Terminal' and created: condition = False - # last_login 改变是最后登录日期, 每次登录都会改变 if instance._meta.object_name == 'User' and \ update_fields and 'last_login' in update_fields: condition = False - # 不在记录白名单中,跳过 if sender._meta.object_name not in MODELS_NEED_RECORD: condition = False return condition @@ -108,7 +104,6 @@ def on_object_pre_create_or_update( return with translation.override('en'): - # users.PrivateToken Model 没有 id 有 pk字段 instance_id = getattr(instance, 'id', getattr(instance, 'pk', None)) instance_before_data = {'id': instance_id} raw_instance = type(instance).objects.filter(pk=instance_id).first() @@ -188,7 +183,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs): 'PermedAsset', 'PermedAccount', 'MenuPermission', 'Permission', 'TicketSession', 'ApplyLoginTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket', - 'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords' + 'FavoriteAsset', 'FavoriteFolder', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords' } include_models = {'UserSession'} for i, app in enumerate(apps.get_models(), 1): diff --git a/apps/rbac/const.py b/apps/rbac/const.py index 88caf2338..fb5507f2c 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -35,6 +35,7 @@ exclude_permissions = ( ('assets', 'cluster', '*', '*'), ('assets', 'systemuser', '*', '*'), ('assets', 'favoriteasset', '*', '*'), + ('assets', 'favoritefolder', '*', '*'), ('assets', 'assetuser', '*', '*'), ('assets', 'web', '*', '*'), ('assets', 'host', '*', '*'),