diff --git a/app/admin_api/serializers/modification_audit.py b/app/admin_api/serializers/modification_audit.py index 3fb9605..978a96e 100644 --- a/app/admin_api/serializers/modification_audit.py +++ b/app/admin_api/serializers/modification_audit.py @@ -2,6 +2,7 @@ import unicodedata from core.serializer.json_schema_serializer import JsonSchemaSerializer +from django.db import transaction from event.presentation.models import Presentation, PresentationSpeaker from participant_portal_api.models import ModificationAudit, ModificationAuditComment from rest_framework import serializers @@ -63,9 +64,10 @@ def validate(self, attrs: dict) -> dict: def save(self, **kwargs: dict) -> ModificationAudit: instance: ModificationAudit = self.instance - instance.status = ModificationAudit.Status.APPROVED - instance.apply_modification() - instance.save() + with transaction.atomic(): + instance.status = ModificationAudit.Status.APPROVED + instance.apply_modification() + instance.save() return instance diff --git a/app/core/test/django_orm_test.py b/app/core/test/django_orm_test.py new file mode 100644 index 0000000..db00583 --- /dev/null +++ b/app/core/test/django_orm_test.py @@ -0,0 +1,28 @@ +import pytest +from core.util.django_orm import apply_diff_to_model, model_to_identifier +from django.utils.translation import override +from event.presentation.models import Presentation +from model_bakery import baker + + +@pytest.mark.parametrize("active_language", ["ko", "en"], ids=["applied_in_ko", "applied_in_en"]) +def test_apply_diff_to_model_ignores_translated_original_field(db, active_language): + presentation = baker.make(Presentation, title_ko="한글제목", title_en="EnglishTitle") + diff = {model_to_identifier(presentation): {"title": "NewEnglishTitle", "title_en": "NewEnglishTitle"}} + + with override(active_language): + apply_diff_to_model(diff) + + presentation.refresh_from_db() + assert presentation.title_en == "NewEnglishTitle" + assert presentation.title_ko == "한글제목" + + +def test_apply_diff_to_model_applies_plain_field(db): + presentation = baker.make(Presentation, slideshow_url="https://example.com/old") + diff = {model_to_identifier(presentation): {"slideshow_url": "https://example.com/new"}} + + apply_diff_to_model(diff) + + presentation.refresh_from_db() + assert presentation.slideshow_url == "https://example.com/new" diff --git a/app/core/util/django_orm.py b/app/core/util/django_orm.py index 93a2d5e..2af8691 100644 --- a/app/core/util/django_orm.py +++ b/app/core/util/django_orm.py @@ -12,6 +12,8 @@ from django.db.models.fields.files import FieldFile from django.forms import model_to_dict from django.utils.functional import Promise +from modeltranslation.translator import NotRegistered +from modeltranslation.translator import translator as modeltranslation_translator def arbitrary_value_to_basic_type(value: typing.Any) -> str | int | float | bool | None: @@ -258,8 +260,17 @@ def apply_diff_to_model(models_data: dict[str, dict[str, typing.Any]]) -> list[m if not (model_instance := identifier_to_model(model_identifier)): raise ValueError(f"Model class not found for identifier: {model_identifier}") + # 원본 필드는 활성 언어로 라우팅되는 descriptor라 적용 시점의 언어에 좌우되므로, 언어별 컬럼만 적용한다. + try: + skip_fields = set(modeltranslation_translator.get_options_for_model(type(model_instance)).fields) + except NotRegistered: + skip_fields = set() + # Apply the data to the model instance for field_name, value in model_data.items(): + if field_name in skip_fields: + continue + if is_identifier(value): # If the value is a model identifier, resolve it to a model instance if not (related_model_instance := identifier_to_model(value)): diff --git a/app/participant_portal_api/test/__init__.py b/app/participant_portal_api/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/participant_portal_api/test/modification_audit_approval_test.py b/app/participant_portal_api/test/modification_audit_approval_test.py new file mode 100644 index 0000000..4fc41c2 --- /dev/null +++ b/app/participant_portal_api/test/modification_audit_approval_test.py @@ -0,0 +1,77 @@ +from contextlib import contextmanager +from types import SimpleNamespace + +import pytest +from core.util.thread_local import thread_local +from django.utils.translation import override +from participant_portal_api.models import ModificationAudit +from participant_portal_api.serializers.user import UserPortalSerializer +from rest_framework import status +from rest_framework.test import APIClient +from user.models import UserExt + +APPROVE_URL = "/v1/admin-api/participant_portal_api/modificationaudit/{audit_id}/approve/" + + +@contextmanager +def _acting_as(user: UserExt): + thread_local.current_request = SimpleNamespace(user=user) + try: + yield + finally: + if hasattr(thread_local, "current_request"): + del thread_local.current_request + + +@pytest.fixture +def requester(db) -> UserExt: + return UserExt.objects.create_user( + username="requester", email="requester@example.com", nickname_ko="한글이름", nickname_en="EnglishName" + ) + + +@pytest.fixture +def superuser(db) -> UserExt: + return UserExt.objects.create_superuser(username="admin", email="admin@example.com") + + +@pytest.fixture +def admin_client(superuser) -> APIClient: + client = APIClient() + client.force_authenticate(user=superuser) + return client + + +def _request_profile_modification(requester: UserExt, payload: dict, language: str) -> ModificationAudit: + with override(language), _acting_as(requester): + serializer = UserPortalSerializer(instance=requester, data=payload, partial=True) + assert serializer.is_valid(), serializer.errors + serializer.save() + + return ModificationAudit.objects.get(status=ModificationAudit.Status.REQUESTED) + + +def test_approve_en_nickname_request_under_ko_locale_keeps_ko_nickname(requester, admin_client): + audit = _request_profile_modification(requester, {"nickname_en": "NewEnglish"}, language="en") + + response = admin_client.patch( + APPROVE_URL.format(audit_id=audit.id), data={}, format="json", HTTP_ACCEPT_LANGUAGE="ko" + ) + + assert response.status_code == status.HTTP_200_OK, response.data + requester.refresh_from_db() + assert requester.nickname_en == "NewEnglish" + assert requester.nickname_ko == "한글이름" + + +def test_approve_ko_nickname_request_under_en_locale_keeps_en_nickname(requester, admin_client): + audit = _request_profile_modification(requester, {"nickname_ko": "새한글이름"}, language="ko") + + response = admin_client.patch( + APPROVE_URL.format(audit_id=audit.id), data={}, format="json", HTTP_ACCEPT_LANGUAGE="en" + ) + + assert response.status_code == status.HTTP_200_OK, response.data + requester.refresh_from_db() + assert requester.nickname_ko == "새한글이름" + assert requester.nickname_en == "EnglishName"