Skip to content

Commit e29c5a4

Browse files
committed
chore: 수정 심사 반영 데이터를 계산하는 방식 변경
1 parent 27657cd commit e29c5a4

File tree

7 files changed

+96
-75
lines changed

7 files changed

+96
-75
lines changed

app/admin_api/serializers/modification_audit.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def validate(self, attrs: dict) -> dict:
4949
def save(self, **kwargs: dict) -> ModificationAudit:
5050
instance: ModificationAudit = self.instance
5151
instance.status = ModificationAudit.Status.APPROVED
52-
instance.apply_modification(save=True)
52+
instance.apply_modification()
53+
instance.save()
5354

5455
return instance
5556

app/admin_api/views/event/presentation.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import annotations
22

3-
import typing
4-
53
from admin_api.filtersets.event.presentation import (
64
PresentationAdminFilterSet,
75
PresentationCategoryAdminFilterSet,
@@ -25,7 +23,7 @@
2523
PresentationType,
2624
)
2725
from participant_portal_api.models import ModificationAudit
28-
from rest_framework import decorators, request, response, status, viewsets
26+
from rest_framework import decorators, response, status, viewsets
2927

3028
ADMIN_METHODS = ["list", "retrieve", "create", "update", "partial_update", "destroy"]
3129

@@ -58,14 +56,13 @@ class PresentationAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
5856
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION])
5957
@decorators.action(detail=True, methods=["get"], url_path=r"preview/(?P<audit_id>[\w-]+)")
6058
def preview_modification_audit(self, audit_id: str, *args: tuple, **kwargs: dict) -> response.Response:
61-
if not UUID_V4_REGEX.match(kwargs.get("audit_id", "")):
59+
if not UUID_V4_REGEX.match(audit_id):
6260
return response.Response(status=status.HTTP_404_NOT_FOUND)
6361

64-
presentation: Presentation = self.get_object()
65-
if not (mod_audit := ModificationAudit.objects.filter_requested(presentation).filter(id=audit_id).first()):
62+
if not (audit := ModificationAudit.objects.filter_by_instance(self.get_object()).filter(id=audit_id).first()):
6663
return response.Response(status=status.HTTP_404_NOT_FOUND)
6764

68-
return response.Response(data=self.get_serializer(mod_audit.apply_modification(save=False)).data)
65+
return response.Response(data=audit.get_applied_data(serializer_class=self.get_serializer()))
6966

7067

7168
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION]) for m in ADMIN_METHODS})
@@ -77,13 +74,12 @@ class PresentationSpeakerAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
7774
queryset = PresentationSpeaker.objects.filter_active().select_related("created_by", "updated_by", "deleted_by")
7875

7976
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION])
80-
@decorators.action(detail=True, methods=["get"], url_path="preview")
81-
def preview_modification_audit(self, request: request.Request, *args: tuple, **kwargs: dict) -> response.Response:
82-
if not (
83-
mod_audit := typing.cast(
84-
ModificationAudit | None, ModificationAudit.objects.filter_requested(self.get_object()).first()
85-
)
86-
):
77+
@decorators.action(detail=True, methods=["get"], url_path=r"preview/(?P<audit_id>[\w-]+)")
78+
def preview_modification_audit(self, audit_id: str, *args: tuple, **kwargs: dict) -> response.Response:
79+
if not UUID_V4_REGEX.match(audit_id):
80+
return response.Response(status=status.HTTP_404_NOT_FOUND)
81+
82+
if not (audit := ModificationAudit.objects.filter_by_instance(self.get_object()).filter(id=audit_id).first()):
8783
return response.Response(status=status.HTTP_404_NOT_FOUND)
8884

89-
return response.Response(data=self.get_serializer(mod_audit.apply_modification(save=False)).data)
85+
return response.Response(data=audit.get_applied_data(serializer_class=self.get_serializer()))

app/admin_api/views/user.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,10 @@ def preview_modification_audit(self, audit_id: str, *args: tuple, **kwargs: dict
8686
if not UUID_V4_REGEX.match(audit_id):
8787
return response.Response(status=status.HTTP_404_NOT_FOUND)
8888

89-
user: UserExt = self.get_object()
90-
if not (mod_audit := ModificationAudit.objects.filter_requested(user).filter(id=audit_id).first()):
89+
if not (audit := ModificationAudit.objects.filter_by_instance(self.get_object()).filter(id=audit_id).first()):
9190
return response.Response(status=status.HTTP_404_NOT_FOUND)
9291

93-
return response.Response(data=self.get_serializer(mod_audit.apply_modification(save=False)).data)
92+
return response.Response(data=audit.get_applied_data(serializer_class=self.get_serializer()))
9493

9594

9695
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_USER]) for m in ADMIN_METHODS})

app/participant_portal_api/models.py

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,62 @@
1+
import contextlib
12
import typing
23

34
from core.models import BaseAbstractModel, BaseAbstractModelQuerySet
45
from django.contrib.contenttypes.fields import GenericForeignKey
56
from django.contrib.contenttypes.models import ContentType
7+
from django.core.exceptions import FieldDoesNotExist
68
from django.db import models
79
from event.presentation.models import Presentation, PresentationSpeaker
10+
from rest_framework import serializers
811
from user.models import UserExt
912

13+
T = typing.TypeVar("T", bound=models.Model)
14+
15+
16+
def _apply_dict_to_model(instance: T, data: dict, save: bool = False) -> T:
17+
model_class = type(instance)
18+
19+
for field_name, value in data.items():
20+
with contextlib.suppress(FieldDoesNotExist):
21+
field = model_class._meta.get_field(field_name)
22+
23+
if isinstance(field, models.ForeignKey):
24+
# One to One or One to Many case
25+
if isinstance(value, dict):
26+
if not (sub_instance := field.related_model.objects.filter(pk=value.get("id")).first()):
27+
continue
28+
setattr(instance, field_name, _apply_dict_to_model(sub_instance, value), save)
29+
elif isinstance(value, (int, str)):
30+
if not (sub_instance := field.related_model.objects.filter(pk=value).first()):
31+
continue
32+
setattr(instance, field_name, sub_instance.pk if field_name.endswith("_id") else sub_instance)
33+
elif isinstance(field, models.ManyToOneRel):
34+
if save:
35+
if not all(isinstance(v, dict) and "id" in v for v in value):
36+
continue
37+
for sub_value in value:
38+
getattr(instance, field).filter(pk=sub_value.pop("id")).update(**sub_value)
39+
else:
40+
# 일반 필드 업데이트
41+
setattr(instance, field_name, value)
42+
43+
if save:
44+
instance.save()
45+
46+
return instance
47+
1048

1149
class ModificationAuditQuerySet(BaseAbstractModelQuerySet):
12-
def filter_requested(self, instance: models.Model) -> typing.Self:
50+
def filter_by_instance(self, instance: models.Model) -> typing.Self:
1351
return self.filter_active().filter(
1452
instance_type__app_label=instance._meta.app_label,
1553
instance_type__model=instance._meta.model_name,
1654
instance_id=str(instance.pk),
17-
status=ModificationAudit.Status.REQUESTED,
1855
)
1956

57+
def filter_requested(self, instance: models.Model) -> typing.Self:
58+
return self.filter_active().filter_by_instance(instance).filter(status=ModificationAudit.Status.REQUESTED)
59+
2060

2161
class ModificationAudit(BaseAbstractModel):
2262
class Action(models.TextChoices):
@@ -58,41 +98,32 @@ class Meta:
5898
def __str__(self) -> str:
5999
return str(self.instance)
60100

61-
def apply_modification(self, save: bool = False) -> models.Model:
62-
for field, value in self.modification_data.items():
63-
if isinstance(value, list):
64-
# One to Many case
65-
sub_value_map = {sub_value["id"]: sub_value for sub_value in value}
66-
if not (sub_instances := list(getattr(self.instance, field).filter(pk__in=sub_value_map))):
67-
continue
101+
def get_applied_data(self, serializer_class: type[serializers.ModelSerializer]) -> dict:
102+
one_to_many: dict[str, dict[str, dict[str, typing.Any]]] = {
103+
k: {sv["id"]: sv for sv in v}
104+
for k, v in self.modification_data.items()
105+
if isinstance(v, list) and all(isinstance(i, dict) and "id" in i for i in v)
106+
}
68107

69-
for sub_instance in sub_instances:
70-
sub_data = sub_value_map[str(sub_instance.pk)]
71-
for sub_field, sub_value in sub_data.items():
72-
setattr(sub_instance, sub_field, sub_value)
108+
modified_instance = _apply_dict_to_model(instance=self.instance, data=self.modification_data, save=False)
109+
modified_data = serializer_class(instance=modified_instance).data
73110

74-
if save:
75-
sub_instance.save()
76-
elif isinstance(value, dict):
77-
# One to One case
78-
if not (sub_instance := getattr(self.instance, field, None)):
79-
continue
111+
for field_name, mod_values in one_to_many.items():
112+
if field_name not in modified_data:
113+
continue
80114

81-
for sub_field, sub_value in value.items():
82-
setattr(sub_instance, sub_field, sub_value)
115+
for field_value in modified_data[field_name]:
116+
if not (isinstance(field_value, dict) and (value_id := field_value.get("id"))):
117+
continue
83118

84-
if save:
85-
sub_instance.save()
86-
else:
87-
setattr(self.instance, field, sub_instance)
88-
else:
89-
# 일반 필드 업데이트
90-
setattr(self.instance, field, value)
119+
if value_id in mod_values:
120+
# 기존 값에 수정된 값을 병합합니다.
121+
field_value.update(mod_values[value_id])
91122

92-
if save:
93-
self.instance.save()
123+
return modified_data
94124

95-
return self.instance
125+
def apply_modification(self) -> models.Model:
126+
return _apply_dict_to_model(instance=self.instance, data=self.modification_data, save=True)
96127

97128

98129
class ModificationAuditComment(BaseAbstractModel):

app/participant_portal_api/serializers/modification_audit.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,8 @@ def validate(self, attrs: dict) -> dict:
128128

129129
def save(self, **kwargs: dict) -> models.Model:
130130
"""instance.save()를 호출하는 대신, 변경된 데이터를 추출하여 ModificationAudit 인스턴스를 생성합니다."""
131-
return ModificationAudit.objects.create(
132-
instance=self.instance, modification_data=self.validated_data
133-
).apply_modification(save=False)
131+
ModificationAudit.objects.create(instance=self.instance, modification_data=self.validated_data)
132+
return self.instance
134133

135134

136135
class ModificationAuditCancelPortalSerializer(serializers.ModelSerializer):

app/participant_portal_api/views/presentation.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from participant_portal_api.models import ModificationAudit
55
from participant_portal_api.permissions import IsSessionSpeaker
66
from participant_portal_api.serializers.presentation import PresentationPortalSerializer
7-
from rest_framework import mixins, viewsets
7+
from rest_framework import mixins, response, viewsets
88

99

1010
@utils.extend_schema_view(
@@ -39,9 +39,14 @@ def get_queryset(self):
3939
)
4040
)
4141

42-
def get_object(self):
43-
presentation = super().get_object()
44-
if mod_audit := ModificationAudit.objects.filter_requested(presentation).first():
45-
presentation = mod_audit.apply_modification(save=False)
42+
def retrieve(self, *args, **kwargs):
43+
"""발표 조회 시, 수정 요청이 있는 경우 해당 요청의 ID를 포함하여 응답"""
44+
instance = self.get_object()
45+
serializer_class = self.get_serializer()
4646

47-
return presentation
47+
if audit := ModificationAudit.objects.filter_requested(instance).first():
48+
data = audit.get_applied_data(serializer_class=serializer_class)
49+
else:
50+
data = serializer_class(instance).data
51+
52+
return response.Response(data=data)

app/participant_portal_api/views/user.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import typing
2-
31
from core.const.tag import OpenAPITag
42
from django.contrib.auth import login, logout
53
from drf_spectacular.utils import extend_schema
@@ -29,10 +27,14 @@ def retrieve_profile(self, request: request.Request, *args: tuple, **kwargs: dic
2927
return response.Response(status=status.HTTP_401_UNAUTHORIZED)
3028

3129
user = request.user
30+
serializer_class = self.get_serializer()
31+
3232
if mod_audit := ModificationAudit.objects.filter_requested(user).first():
33-
user = mod_audit.apply_modification(save=False)
33+
data = mod_audit.get_applied_data(serializer_class=serializer_class)
34+
else:
35+
data = serializer_class(user).data
3436

35-
return response.Response(data=self.get_serializer(user).data)
37+
return response.Response(data=data)
3638

3739
@extend_schema(
3840
tags=[OpenAPITag.PARTICIPANT_PORTAL_USER],
@@ -49,18 +51,6 @@ def patch_profile(self, request: request.Request, *args: tuple, **kwargs: dict)
4951

5052
return response.Response(data=UserPortalSerializer(instance).data)
5153

52-
@extend_schema(tags=[OpenAPITag.PARTICIPANT_PORTAL_PRESENTATION])
53-
@decorators.action(detail=False, methods=["get"], url_path="me/preview")
54-
def preview_modification_audit(self, request: request.Request, *args: tuple, **kwargs: dict) -> response.Response:
55-
if not (
56-
mod_audit := typing.cast(
57-
ModificationAudit | None, ModificationAudit.objects.filter_requested(request.user).first()
58-
)
59-
):
60-
return response.Response(status=status.HTTP_404_NOT_FOUND)
61-
62-
return response.Response(data=self.get_serializer(mod_audit.apply_modification(save=False)).data)
63-
6454
@extend_schema(
6555
tags=[OpenAPITag.PARTICIPANT_PORTAL_USER],
6656
request=UserPortalSignInSerializer,

0 commit comments

Comments
 (0)