Skip to content

Commit 83bdc10

Browse files
committed
fix: 수정 심사 승인 시 관계 필드 적용이 실패하던 문제 수정
1 parent 6e0ecec commit 83bdc10

3 files changed

Lines changed: 80 additions & 14 deletions

File tree

‎app/core/test/django_orm_test.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,38 @@ def test_apply_diff_to_model_applies_plain_field(db):
3838
assert presentation.slideshow_url == "https://example.com/new"
3939

4040

41+
def test_apply_diff_to_model_ignores_reordered_reverse_relation(db):
42+
presentation = baker.make(Presentation)
43+
speakers = baker.make(PresentationSpeaker, presentation=presentation, _quantity=2)
44+
diff = {model_to_identifier(presentation): {"speakers": [model_to_identifier(s) for s in reversed(speakers)]}}
45+
46+
apply_diff_to_model(diff)
47+
48+
assert set(presentation.speakers.values_list("id", flat=True)) == {s.pk for s in speakers}
49+
50+
51+
def test_apply_diff_to_model_rejects_member_change_on_non_nullable_reverse_relation(db):
52+
presentation = baker.make(Presentation)
53+
speakers = baker.make(PresentationSpeaker, presentation=presentation, _quantity=2)
54+
diff = {model_to_identifier(presentation): {"speakers": [model_to_identifier(speakers[0])]}}
55+
56+
with pytest.raises(ValueError, match="speakers"):
57+
apply_diff_to_model(diff)
58+
59+
assert presentation.speakers.count() == 2
60+
61+
62+
def test_apply_diff_to_model_applies_many_to_many_members(db):
63+
presentation = baker.make(Presentation)
64+
old_category, new_category = baker.make(PresentationCategory, type=presentation.type, _quantity=2)
65+
baker.make(PresentationCategoryRelation, presentation=presentation, category=old_category)
66+
diff = {model_to_identifier(presentation): {"categories": [model_to_identifier(new_category)]}}
67+
68+
apply_diff_to_model(diff)
69+
70+
assert set(presentation.categories.values_list("id", flat=True)) == {new_category.pk}
71+
72+
4173
def test_model_to_jsonable_dict_sorts_reverse_relation_identifiers(db):
4274
presentation = baker.make(Presentation)
4375
speakers = baker.make(PresentationSpeaker, presentation=presentation, _quantity=3)

‎app/core/util/django_orm.py‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,21 @@ def json_to_simplenamespace(model_data: dict[str, dict[str, typing.Any]], key: s
262262
return resolved_models[key]
263263

264264

265+
def _apply_related_diff(manager: models.manager.BaseManager, field_name: str, identifiers: list[str]) -> None:
266+
related_model_instances = [identifier_to_model(item) for item in identifiers]
267+
if None in related_model_instances:
268+
raise ValueError(f"One or more related models not found for identifiers: {identifiers}")
269+
270+
if {item.pk for item in manager.all()} == {item.pk for item in related_model_instances}:
271+
return
272+
273+
# 역참조 FK의 remove()는 Django가 field.null=True일 때만 제공하므로, 그 외에는 구성원 변경을 적용할 수 없다.
274+
if not hasattr(manager, "remove"):
275+
raise ValueError(f"Cannot change members of the non-nullable reverse relation '{field_name}'")
276+
277+
manager.set(related_model_instances)
278+
279+
265280
def apply_diff_to_model(models_data: dict[str, dict[str, typing.Any]]) -> list[models.Model]:
266281
result_instances: list[models.Model] = []
267282

@@ -285,20 +300,11 @@ def apply_diff_to_model(models_data: dict[str, dict[str, typing.Any]]) -> list[m
285300
if not (related_model_instance := identifier_to_model(value)):
286301
raise ValueError(f"Related model not found for identifier: {value}")
287302
setattr(model_instance, field_name, related_model_instance)
288-
elif isinstance(value, collections.abc.Iterable) and value and all(is_identifier(item) for item in value):
289-
# If the value is a list of model identifiers, resolve them to model instances
290-
related_model_instances = [identifier_to_model(item) for item in value]
291-
if None in related_model_instances:
292-
raise ValueError(f"One or more related models not found for identifiers: {value}")
293-
294-
old_related_models = {item.pk: item for item in getattr(model_instance, field_name, [])}
295-
new_related_models = {item.pk: item for item in related_model_instances}
296-
297-
field = getattr(model_instance, field_name)
298-
for del_pk in old_related_models.keys() - new_related_models.keys():
299-
field.remove(old_related_models[del_pk])
300-
for add_pk in new_related_models.keys() - old_related_models.keys():
301-
field.add(new_related_models[add_pk])
303+
elif is_identifier_list(value) and isinstance(
304+
related_manager := getattr(model_instance, field_name, None), models.manager.BaseManager
305+
):
306+
# If the value is a list of model identifiers, it is an m2m/reverse relation
307+
_apply_related_diff(related_manager, field_name, value)
302308
else:
303309
setattr(model_instance, field_name, value)
304310

‎app/participant_portal_api/test/modification_audit_approval_test.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
from types import SimpleNamespace
33

44
import pytest
5+
from core.util.django_orm import model_to_identifier
56
from core.util.thread_local import thread_local
7+
from django.contrib.contenttypes.models import ContentType
68
from django.utils.translation import override
9+
from event.presentation.models import Presentation, PresentationSpeaker
10+
from model_bakery import baker
711
from participant_portal_api.models import ModificationAudit
812
from participant_portal_api.serializers.user import UserPortalSerializer
913
from rest_framework import status
@@ -75,3 +79,27 @@ def test_approve_ko_nickname_request_under_en_locale_keeps_en_nickname(requester
7579
requester.refresh_from_db()
7680
assert requester.nickname_ko == "새한글이름"
7781
assert requester.nickname_en == "EnglishName"
82+
83+
84+
def test_approve_presentation_audit_with_reordered_speakers(db, admin_client):
85+
"""발표자 순서만 뒤집힌 채 저장된 과거 수정 요청도 승인할 수 있어야 한다."""
86+
presentation = baker.make(Presentation)
87+
speakers = baker.make(PresentationSpeaker, presentation=presentation, biography_ko="이전 소개", _quantity=2)
88+
audit = ModificationAudit.objects.create(
89+
instance_type=ContentType.objects.get_for_model(Presentation),
90+
instance_id=str(presentation.pk),
91+
original_data={model_to_identifier(presentation): {}},
92+
modification_data={
93+
model_to_identifier(presentation): {
94+
"speakers": [model_to_identifier(speaker) for speaker in reversed(speakers)]
95+
},
96+
model_to_identifier(speakers[0]): {"biography_ko": "새 소개"},
97+
},
98+
)
99+
100+
response = admin_client.patch(APPROVE_URL.format(audit_id=audit.id), data={}, format="json")
101+
102+
assert response.status_code == status.HTTP_200_OK, response.data
103+
speakers[0].refresh_from_db()
104+
assert speakers[0].biography_ko == "새 소개"
105+
assert set(presentation.speakers.values_list("id", flat=True)) == {speaker.pk for speaker in speakers}

0 commit comments

Comments
 (0)