fix: 修复换火山账号导致素材库操作失败问题
This commit is contained in:
@@ -20,9 +20,15 @@ _ASSETS_ERROR_MESSAGES = {
|
|||||||
"RequestError": "素材审核服务暂时不可用,请稍后重试",
|
"RequestError": "素材审核服务暂时不可用,请稍后重试",
|
||||||
"InvalidParameter": "素材参数无效",
|
"InvalidParameter": "素材参数无效",
|
||||||
"NotFound": "素材不存在或已被删除",
|
"NotFound": "素材不存在或已被删除",
|
||||||
|
"NotFound.asset_id": "素材不存在或已被删除",
|
||||||
|
"NotFound.group_id": "素材组不存在或已被删除",
|
||||||
"Forbidden": "没有权限操作该素材(检查 AK/SK)",
|
"Forbidden": "没有权限操作该素材(检查 AK/SK)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_NOT_FOUND_CODES = frozenset({"NotFound", "NotFound.asset_id", "NotFound.group_id"})
|
||||||
|
_ASSET_NOT_FOUND_CODES = frozenset({"NotFound", "NotFound.asset_id"})
|
||||||
|
_GROUP_NOT_FOUND_CODES = frozenset({"NotFound", "NotFound.group_id"})
|
||||||
|
|
||||||
|
|
||||||
class AssetsAPIError(Exception):
|
class AssetsAPIError(Exception):
|
||||||
def __init__(self, code, message, status_code=400):
|
def __init__(self, code, message, status_code=400):
|
||||||
@@ -33,6 +39,18 @@ class AssetsAPIError(Exception):
|
|||||||
super().__init__(f"[{code}] {message}")
|
super().__init__(f"[{code}] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_not_found_code(code: str | None) -> bool:
|
||||||
|
return code in _NOT_FOUND_CODES
|
||||||
|
|
||||||
|
|
||||||
|
def is_asset_not_found_code(code: str | None) -> bool:
|
||||||
|
return code in _ASSET_NOT_FOUND_CODES
|
||||||
|
|
||||||
|
|
||||||
|
def is_group_not_found_code(code: str | None) -> bool:
|
||||||
|
return code in _GROUP_NOT_FOUND_CODES
|
||||||
|
|
||||||
|
|
||||||
def is_enabled() -> bool:
|
def is_enabled() -> bool:
|
||||||
cfg = getattr(settings, "ASSETS_API", {}) or {}
|
cfg = getattr(settings, "ASSETS_API", {}) or {}
|
||||||
return bool(cfg.get("enabled") and cfg.get("access_key") and cfg.get("secret_key"))
|
return bool(cfg.get("enabled") and cfg.get("access_key") and cfg.get("secret_key"))
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ _VIDEO_MAX = 50 * 1024 * 1024
|
|||||||
_AUDIO_MAX = 15 * 1024 * 1024
|
_AUDIO_MAX = 15 * 1024 * 1024
|
||||||
DEFAULT_GROUP_NAME = "默认素材"
|
DEFAULT_GROUP_NAME = "默认素材"
|
||||||
DEFAULT_GROUP_MARKER = "__airshelf_free_asset_default__"
|
DEFAULT_GROUP_MARKER = "__airshelf_free_asset_default__"
|
||||||
|
OTHER_REMOTE_ASSET_MESSAGE = "该素材属于其他火山账号,请重新上传后再编辑"
|
||||||
|
OTHER_REMOTE_GROUP_MESSAGE = "该素材组属于其他火山账号,请新建素材组后重新上传"
|
||||||
|
|
||||||
|
|
||||||
def _serialize_asset(fa: FreeAsset) -> dict:
|
def _serialize_asset(fa: FreeAsset) -> dict:
|
||||||
@@ -63,10 +65,7 @@ def _assets_api_unavailable() -> Response:
|
|||||||
return Response({"detail": "素材库服务未配置,请联系管理员"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
return Response({"detail": "素材库服务未配置,请联系管理员"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||||
|
|
||||||
|
|
||||||
def _get_or_create_default_group(*, team, user) -> FreeAssetGroup:
|
def _create_default_group(*, team, user) -> FreeAssetGroup:
|
||||||
group = FreeAssetGroup.objects.filter(team=team, is_deleted=False, description=DEFAULT_GROUP_MARKER).first()
|
|
||||||
if group is not None:
|
|
||||||
return group
|
|
||||||
remote_group_id = assets_client.create_asset_group(DEFAULT_GROUP_NAME, DEFAULT_GROUP_MARKER)
|
remote_group_id = assets_client.create_asset_group(DEFAULT_GROUP_NAME, DEFAULT_GROUP_MARKER)
|
||||||
return FreeAssetGroup.objects.create(
|
return FreeAssetGroup.objects.create(
|
||||||
team=team,
|
team=team,
|
||||||
@@ -77,6 +76,19 @@ def _get_or_create_default_group(*, team, user) -> FreeAssetGroup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_or_create_default_group(*, team, user) -> FreeAssetGroup:
|
||||||
|
group = FreeAssetGroup.objects.filter(team=team, is_deleted=False, description=DEFAULT_GROUP_MARKER).first()
|
||||||
|
if group is not None:
|
||||||
|
try:
|
||||||
|
assets_client.get_asset_group(group.remote_group_id)
|
||||||
|
except AssetsAPIError as exc:
|
||||||
|
if not assets_client.is_group_not_found_code(exc.code):
|
||||||
|
raise
|
||||||
|
return _create_default_group(team=team, user=user)
|
||||||
|
return group
|
||||||
|
return _create_default_group(team=team, user=user)
|
||||||
|
|
||||||
|
|
||||||
def _upload_asset_to_group(request, *, team, group: FreeAssetGroup, image_only: bool = False) -> Response:
|
def _upload_asset_to_group(request, *, team, group: FreeAssetGroup, image_only: bool = False) -> Response:
|
||||||
upload = request.FILES.get("file")
|
upload = request.FILES.get("file")
|
||||||
if upload is None:
|
if upload is None:
|
||||||
@@ -138,6 +150,8 @@ def _upload_asset_to_group(request, *, team, group: FreeAssetGroup, image_only:
|
|||||||
try:
|
try:
|
||||||
remote_asset_id = assets_client.create_asset(group.remote_group_id, url, name=name, asset_type=asset_type)
|
remote_asset_id = assets_client.create_asset(group.remote_group_id, url, name=name, asset_type=asset_type)
|
||||||
except AssetsAPIError as exc:
|
except AssetsAPIError as exc:
|
||||||
|
if assets_client.is_group_not_found_code(exc.code):
|
||||||
|
return Response({"detail": OTHER_REMOTE_GROUP_MESSAGE}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
fa = FreeAsset.objects.create(
|
fa = FreeAsset.objects.create(
|
||||||
@@ -215,7 +229,8 @@ class FreeAssetGroupDetailView(APIView):
|
|||||||
try:
|
try:
|
||||||
assets_client.update_asset_group(group.remote_group_id, name=group.name, description=group.description)
|
assets_client.update_asset_group(group.remote_group_id, name=group.name, description=group.description)
|
||||||
except AssetsAPIError as exc:
|
except AssetsAPIError as exc:
|
||||||
if exc.code != "NotFound":
|
if assets_client.is_group_not_found_code(exc.code):
|
||||||
|
return Response({"detail": OTHER_REMOTE_GROUP_MESSAGE}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
group.save(update_fields=update_fields)
|
group.save(update_fields=update_fields)
|
||||||
return Response({"group": _serialize_group(group)})
|
return Response({"group": _serialize_group(group)})
|
||||||
@@ -228,7 +243,7 @@ class FreeAssetGroupDetailView(APIView):
|
|||||||
assets_client.delete_asset_group(group.remote_group_id)
|
assets_client.delete_asset_group(group.remote_group_id)
|
||||||
except AssetsAPIError as exc:
|
except AssetsAPIError as exc:
|
||||||
# 远程已不存在 → 幂等继续清本地;其它错误如实反馈
|
# 远程已不存在 → 幂等继续清本地;其它错误如实反馈
|
||||||
if exc.code != "NotFound":
|
if not assets_client.is_group_not_found_code(exc.code):
|
||||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
group.is_deleted = True
|
group.is_deleted = True
|
||||||
group.save(update_fields=["is_deleted", "updated_at"])
|
group.save(update_fields=["is_deleted", "updated_at"])
|
||||||
@@ -286,7 +301,8 @@ class FreeAssetDetailView(APIView):
|
|||||||
try:
|
try:
|
||||||
assets_client.update_asset(fa.remote_asset_id, name=name)
|
assets_client.update_asset(fa.remote_asset_id, name=name)
|
||||||
except AssetsAPIError as exc:
|
except AssetsAPIError as exc:
|
||||||
if exc.code != "NotFound":
|
if assets_client.is_asset_not_found_code(exc.code):
|
||||||
|
return Response({"detail": OTHER_REMOTE_ASSET_MESSAGE}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
fa.name = name[:128]
|
fa.name = name[:128]
|
||||||
fa.save(update_fields=["name", "updated_at"])
|
fa.save(update_fields=["name", "updated_at"])
|
||||||
@@ -299,7 +315,7 @@ class FreeAssetDetailView(APIView):
|
|||||||
try:
|
try:
|
||||||
assets_client.delete_asset(fa.remote_asset_id)
|
assets_client.delete_asset(fa.remote_asset_id)
|
||||||
except AssetsAPIError as exc:
|
except AssetsAPIError as exc:
|
||||||
if exc.code != "NotFound":
|
if not assets_client.is_asset_not_found_code(exc.code):
|
||||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
fa.delete()
|
fa.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase
|
from django.test import SimpleTestCase, TestCase
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from apps.accounts.models import Team, TeamMember, User
|
from apps.accounts.models import Team, TeamMember, User
|
||||||
|
|
||||||
|
from . import assets_client
|
||||||
from .models import Asset, FreeAsset, FreeAssetGroup, Model
|
from .models import Asset, FreeAsset, FreeAssetGroup, Model
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +153,32 @@ class AssetTrashApiTests(TestCase):
|
|||||||
self.assertEqual(self.client.post(f"/api/assets/{self.live.id}/restore/").status_code, 404)
|
self.assertEqual(self.client.post(f"/api/assets/{self.live.id}/restore/").status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class AssetsClientErrorCodeTests(SimpleTestCase):
|
||||||
|
def test_not_found_helpers_cover_volcano_variants(self):
|
||||||
|
self.assertTrue(assets_client.is_not_found_code("NotFound"))
|
||||||
|
self.assertTrue(assets_client.is_not_found_code("NotFound.asset_id"))
|
||||||
|
self.assertTrue(assets_client.is_not_found_code("NotFound.group_id"))
|
||||||
|
self.assertFalse(assets_client.is_not_found_code("Forbidden"))
|
||||||
|
self.assertFalse(assets_client.is_not_found_code(None))
|
||||||
|
|
||||||
|
self.assertTrue(assets_client.is_asset_not_found_code("NotFound"))
|
||||||
|
self.assertTrue(assets_client.is_asset_not_found_code("NotFound.asset_id"))
|
||||||
|
self.assertFalse(assets_client.is_asset_not_found_code("NotFound.group_id"))
|
||||||
|
|
||||||
|
self.assertTrue(assets_client.is_group_not_found_code("NotFound"))
|
||||||
|
self.assertTrue(assets_client.is_group_not_found_code("NotFound.group_id"))
|
||||||
|
self.assertFalse(assets_client.is_group_not_found_code("NotFound.asset_id"))
|
||||||
|
|
||||||
|
def test_not_found_variant_messages_are_not_generic(self):
|
||||||
|
asset_error = assets_client.AssetsAPIError("NotFound.asset_id", "remote asset missing")
|
||||||
|
group_error = assets_client.AssetsAPIError("NotFound.group_id", "remote group missing")
|
||||||
|
unknown_error = assets_client.AssetsAPIError("Unknown", "unknown")
|
||||||
|
|
||||||
|
self.assertNotEqual(asset_error.user_message, unknown_error.user_message)
|
||||||
|
self.assertNotEqual(group_error.user_message, unknown_error.user_message)
|
||||||
|
self.assertNotEqual(asset_error.user_message, group_error.user_message)
|
||||||
|
|
||||||
|
|
||||||
class FreeAssetQuickUploadTests(TestCase):
|
class FreeAssetQuickUploadTests(TestCase):
|
||||||
"""人物素材库快捷上传:自动默认组,但底层仍走 FreeAssetGroup / FreeAsset。"""
|
"""人物素材库快捷上传:自动默认组,但底层仍走 FreeAssetGroup / FreeAsset。"""
|
||||||
|
|
||||||
@@ -187,10 +214,11 @@ class FreeAssetQuickUploadTests(TestCase):
|
|||||||
self.assertEqual(res.json()["group"]["id"], str(group.id))
|
self.assertEqual(res.json()["group"]["id"], str(group.id))
|
||||||
|
|
||||||
@patch("apps.assets.free_library.assets_client.is_enabled", return_value=True)
|
@patch("apps.assets.free_library.assets_client.is_enabled", return_value=True)
|
||||||
|
@patch("apps.assets.free_library.assets_client.get_asset_group", return_value={"Id": "Group-1"})
|
||||||
@patch("apps.assets.free_library.assets_client.create_asset_group", return_value="Group-1")
|
@patch("apps.assets.free_library.assets_client.create_asset_group", return_value="Group-1")
|
||||||
@patch("apps.assets.free_library.assets_client.create_asset", side_effect=["Asset-1", "Asset-2"])
|
@patch("apps.assets.free_library.assets_client.create_asset", side_effect=["Asset-1", "Asset-2"])
|
||||||
@patch("apps.assets.free_library.TosStorage")
|
@patch("apps.assets.free_library.TosStorage")
|
||||||
def test_quick_upload_reuses_default_group(self, storage_cls, create_asset, create_group, _enabled):
|
def test_quick_upload_reuses_default_group(self, storage_cls, create_asset, create_group, get_group, _enabled):
|
||||||
storage = storage_cls.return_value
|
storage = storage_cls.return_value
|
||||||
storage.upload_fileobj.return_value = self.stored
|
storage.upload_fileobj.return_value = self.stored
|
||||||
storage.public_url.return_value = "http://tos/a.png"
|
storage.public_url.return_value = "http://tos/a.png"
|
||||||
@@ -202,8 +230,40 @@ class FreeAssetQuickUploadTests(TestCase):
|
|||||||
self.assertEqual(FreeAssetGroup.objects.filter(team=self.team).count(), 1)
|
self.assertEqual(FreeAssetGroup.objects.filter(team=self.team).count(), 1)
|
||||||
self.assertEqual(FreeAsset.objects.filter(group__team=self.team).count(), 2)
|
self.assertEqual(FreeAsset.objects.filter(group__team=self.team).count(), 2)
|
||||||
create_group.assert_called_once()
|
create_group.assert_called_once()
|
||||||
|
get_group.assert_called_once_with("Group-1")
|
||||||
self.assertEqual(create_asset.call_count, 2)
|
self.assertEqual(create_asset.call_count, 2)
|
||||||
|
|
||||||
|
@patch("apps.assets.free_library.assets_client.is_enabled", return_value=True)
|
||||||
|
@patch(
|
||||||
|
"apps.assets.free_library.assets_client.get_asset_group",
|
||||||
|
side_effect=assets_client.AssetsAPIError("NotFound.group_id", "old group not visible"),
|
||||||
|
)
|
||||||
|
@patch("apps.assets.free_library.assets_client.create_asset_group", return_value="Group-2")
|
||||||
|
@patch("apps.assets.free_library.assets_client.create_asset", return_value="Asset-2")
|
||||||
|
@patch("apps.assets.free_library.TosStorage")
|
||||||
|
def test_quick_upload_replaces_old_default_group(self, storage_cls, create_asset, create_group, get_group, _enabled):
|
||||||
|
old_group = FreeAssetGroup.objects.create(
|
||||||
|
team=self.team,
|
||||||
|
created_by=self.user,
|
||||||
|
name="默认素材",
|
||||||
|
description="__airshelf_free_asset_default__",
|
||||||
|
remote_group_id="Old-Group",
|
||||||
|
)
|
||||||
|
storage = storage_cls.return_value
|
||||||
|
storage.upload_fileobj.return_value = self.stored
|
||||||
|
storage.public_url.return_value = "http://tos/a.png"
|
||||||
|
|
||||||
|
res = self.client.post("/api/assets/free-assets/quick-upload/", {"file": self._image("new.png")}, format="multipart")
|
||||||
|
|
||||||
|
self.assertEqual(res.status_code, 201)
|
||||||
|
self.assertFalse(FreeAsset.objects.filter(group=old_group).exists())
|
||||||
|
new_group = FreeAssetGroup.objects.exclude(id=old_group.id).get(team=self.team)
|
||||||
|
self.assertEqual(new_group.remote_group_id, "Group-2")
|
||||||
|
self.assertEqual(new_group.assets.count(), 1)
|
||||||
|
get_group.assert_called_once_with("Old-Group")
|
||||||
|
create_group.assert_called_once()
|
||||||
|
create_asset.assert_called_once_with("Group-2", "http://tos/a.png", name="new.png", asset_type=FreeAsset.Type.IMAGE)
|
||||||
|
|
||||||
@patch("apps.assets.free_library.assets_client.is_enabled", return_value=True)
|
@patch("apps.assets.free_library.assets_client.is_enabled", return_value=True)
|
||||||
@patch("apps.assets.free_library.assets_client.create_asset_group", return_value="Group-1")
|
@patch("apps.assets.free_library.assets_client.create_asset_group", return_value="Group-1")
|
||||||
@patch("apps.assets.free_library.TosStorage")
|
@patch("apps.assets.free_library.TosStorage")
|
||||||
@@ -219,6 +279,143 @@ class FreeAssetQuickUploadTests(TestCase):
|
|||||||
create_group.assert_called_once()
|
create_group.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class FreeAssetGroupUploadTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user, self.team = _mk_team("fgu", "TeamFGU")
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.force_authenticate(self.user)
|
||||||
|
self.group = FreeAssetGroup.objects.create(
|
||||||
|
team=self.team,
|
||||||
|
created_by=self.user,
|
||||||
|
name="旧素材组",
|
||||||
|
description="",
|
||||||
|
remote_group_id="Old-Group",
|
||||||
|
)
|
||||||
|
self.stored = SimpleNamespace(object_key="teams/x/free-create/library/a.png", bucket="b", content_type="image/png", size_bytes=10)
|
||||||
|
|
||||||
|
def _image(self, name="a.png"):
|
||||||
|
return SimpleUploadedFile(name, b"img", content_type="image/png")
|
||||||
|
|
||||||
|
@patch("apps.assets.free_library.assets_client.is_enabled", return_value=True)
|
||||||
|
@patch(
|
||||||
|
"apps.assets.free_library.assets_client.create_asset",
|
||||||
|
side_effect=assets_client.AssetsAPIError("NotFound.group_id", "old group not visible"),
|
||||||
|
)
|
||||||
|
@patch("apps.assets.free_library.TosStorage")
|
||||||
|
def test_upload_to_old_remote_group_returns_clear_message(self, storage_cls, create_asset, _enabled):
|
||||||
|
storage = storage_cls.return_value
|
||||||
|
storage.upload_fileobj.return_value = self.stored
|
||||||
|
storage.public_url.return_value = "http://tos/a.png"
|
||||||
|
|
||||||
|
res = self.client.post(f"/api/assets/free-groups/{self.group.id}/assets/", {"file": self._image()}, format="multipart")
|
||||||
|
|
||||||
|
self.assertEqual(res.status_code, 400)
|
||||||
|
self.assertEqual(res.json()["detail"], "该素材组属于其他火山账号,请新建素材组后重新上传")
|
||||||
|
self.assertFalse(FreeAsset.objects.filter(group=self.group).exists())
|
||||||
|
create_asset.assert_called_once_with("Old-Group", "http://tos/a.png", name="a.png", asset_type=FreeAsset.Type.IMAGE)
|
||||||
|
|
||||||
|
|
||||||
|
class FreeAssetDeleteCompatibilityTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user, self.team = _mk_team("fdel", "TeamFDEL")
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.force_authenticate(self.user)
|
||||||
|
self.group = FreeAssetGroup.objects.create(
|
||||||
|
team=self.team,
|
||||||
|
created_by=self.user,
|
||||||
|
name="旧素材组",
|
||||||
|
description="",
|
||||||
|
remote_group_id="Old-Group",
|
||||||
|
)
|
||||||
|
self.asset = FreeAsset.objects.create(
|
||||||
|
group=self.group,
|
||||||
|
remote_asset_id="Old-Asset",
|
||||||
|
name="旧素材.png",
|
||||||
|
url="http://tos/a.png",
|
||||||
|
asset_type=FreeAsset.Type.IMAGE,
|
||||||
|
thumbnail_url="http://tos/a.png",
|
||||||
|
status=FreeAsset.Status.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"apps.assets.free_library.assets_client.delete_asset",
|
||||||
|
side_effect=assets_client.AssetsAPIError("NotFound.asset_id", "old asset not visible"),
|
||||||
|
)
|
||||||
|
def test_delete_old_remote_asset_clears_local_asset(self, delete_asset):
|
||||||
|
res = self.client.delete(f"/api/assets/free-assets/{self.asset.id}/")
|
||||||
|
|
||||||
|
self.assertEqual(res.status_code, 204)
|
||||||
|
self.assertFalse(FreeAsset.objects.filter(id=self.asset.id).exists())
|
||||||
|
delete_asset.assert_called_once_with("Old-Asset")
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"apps.assets.free_library.assets_client.delete_asset_group",
|
||||||
|
side_effect=assets_client.AssetsAPIError("NotFound.group_id", "old group not visible"),
|
||||||
|
)
|
||||||
|
def test_delete_old_remote_group_marks_local_group_deleted(self, delete_group):
|
||||||
|
res = self.client.delete(f"/api/assets/free-groups/{self.group.id}/")
|
||||||
|
|
||||||
|
self.assertEqual(res.status_code, 204)
|
||||||
|
self.group.refresh_from_db()
|
||||||
|
self.assertTrue(self.group.is_deleted)
|
||||||
|
self.assertTrue(FreeAsset.objects.filter(id=self.asset.id).exists())
|
||||||
|
delete_group.assert_called_once_with("Old-Group")
|
||||||
|
|
||||||
|
|
||||||
|
class FreeAssetRenameCompatibilityTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user, self.team = _mk_team("fren", "TeamFREN")
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.force_authenticate(self.user)
|
||||||
|
self.group = FreeAssetGroup.objects.create(
|
||||||
|
team=self.team,
|
||||||
|
created_by=self.user,
|
||||||
|
name="旧素材组",
|
||||||
|
description="old description",
|
||||||
|
remote_group_id="Old-Group",
|
||||||
|
)
|
||||||
|
self.asset = FreeAsset.objects.create(
|
||||||
|
group=self.group,
|
||||||
|
remote_asset_id="Old-Asset",
|
||||||
|
name="旧素材.png",
|
||||||
|
url="http://tos/a.png",
|
||||||
|
asset_type=FreeAsset.Type.IMAGE,
|
||||||
|
thumbnail_url="http://tos/a.png",
|
||||||
|
status=FreeAsset.Status.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"apps.assets.free_library.assets_client.update_asset",
|
||||||
|
side_effect=assets_client.AssetsAPIError("NotFound.asset_id", "old asset not visible"),
|
||||||
|
)
|
||||||
|
def test_rename_old_remote_asset_returns_clear_message_without_saving(self, update_asset):
|
||||||
|
res = self.client.patch(f"/api/assets/free-assets/{self.asset.id}/", {"name": "新名字.png"}, format="json")
|
||||||
|
|
||||||
|
self.assertEqual(res.status_code, 400)
|
||||||
|
self.assertEqual(res.json()["detail"], "该素材属于其他火山账号,请重新上传后再编辑")
|
||||||
|
self.asset.refresh_from_db()
|
||||||
|
self.assertEqual(self.asset.name, "旧素材.png")
|
||||||
|
update_asset.assert_called_once_with("Old-Asset", name="新名字.png")
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"apps.assets.free_library.assets_client.update_asset_group",
|
||||||
|
side_effect=assets_client.AssetsAPIError("NotFound.group_id", "old group not visible"),
|
||||||
|
)
|
||||||
|
def test_rename_old_remote_group_returns_clear_message_without_saving(self, update_group):
|
||||||
|
res = self.client.patch(
|
||||||
|
f"/api/assets/free-groups/{self.group.id}/",
|
||||||
|
{"name": "新素材组", "description": "new description"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(res.status_code, 400)
|
||||||
|
self.assertEqual(res.json()["detail"], "该素材组属于其他火山账号,请新建素材组后重新上传")
|
||||||
|
self.group.refresh_from_db()
|
||||||
|
self.assertEqual(self.group.name, "旧素材组")
|
||||||
|
self.assertEqual(self.group.description, "old description")
|
||||||
|
update_group.assert_called_once_with("Old-Group", name="新素材组", description="new description")
|
||||||
|
|
||||||
|
|
||||||
class VideoPacksTests(TestCase):
|
class VideoPacksTests(TestCase):
|
||||||
"""视频成品按项目素材包分组(与导出同源:取 VideoSegment.adopted_version 的真实视频资产)。"""
|
"""视频成品按项目素材包分组(与导出同源:取 VideoSegment.adopted_version 的真实视频资产)。"""
|
||||||
|
|
||||||
|
|||||||
@@ -431,11 +431,12 @@ export function ToastLike({ notice }: { notice: NonNullable<Notice> }) {
|
|||||||
return () => cancelAnimationFrame(raf);
|
return () => cancelAnimationFrame(raf);
|
||||||
}, []);
|
}, []);
|
||||||
const Icon = notice.type === "error" ? AlertCircle : notice.type === "info" ? Info : Check;
|
const Icon = notice.type === "error" ? AlertCircle : notice.type === "info" ? Info : Check;
|
||||||
return (
|
return createPortal(
|
||||||
<div className={`toast${show ? " show" : ""}`} role="status" aria-live="polite">
|
<div className={`toast${show ? " show" : ""}`} role="status" aria-live="polite">
|
||||||
<div className="ic-t"><Icon size={13} /></div>
|
<div className="ic-t"><Icon size={13} /></div>
|
||||||
<div className="txt">{notice.text}<span className="mono">[ {notice.type.toUpperCase()} ]</span></div>
|
<div className="txt">{notice.text}<span className="mono">[ {notice.type.toUpperCase()} ]</span></div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1728,7 +1728,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
|||||||
box-shadow: var(--shadow-floating);
|
box-shadow: var(--shadow-floating);
|
||||||
transform: translateX(420px);
|
transform: translateX(420px);
|
||||||
transition: transform var(--t-slow);
|
transition: transform var(--t-slow);
|
||||||
z-index: 1000;
|
z-index: 10000;
|
||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
}
|
}
|
||||||
.toast.show { transform: translateX(0); }
|
.toast.show { transform: translateX(0); }
|
||||||
|
|||||||
Reference in New Issue
Block a user