|
| 1 | +import contextlib |
1 | 2 | import typing |
2 | 3 |
|
3 | 4 | from core.models import BaseAbstractModel, BaseAbstractModelQuerySet |
4 | 5 | from django.contrib.contenttypes.fields import GenericForeignKey |
5 | 6 | from django.contrib.contenttypes.models import ContentType |
| 7 | +from django.core.exceptions import FieldDoesNotExist |
6 | 8 | from django.db import models |
7 | 9 | from event.presentation.models import Presentation, PresentationSpeaker |
| 10 | +from rest_framework import serializers |
8 | 11 | from user.models import UserExt |
9 | 12 |
|
| 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 | + |
10 | 48 |
|
11 | 49 | class ModificationAuditQuerySet(BaseAbstractModelQuerySet): |
12 | | - def filter_requested(self, instance: models.Model) -> typing.Self: |
| 50 | + def filter_by_instance(self, instance: models.Model) -> typing.Self: |
13 | 51 | return self.filter_active().filter( |
14 | 52 | instance_type__app_label=instance._meta.app_label, |
15 | 53 | instance_type__model=instance._meta.model_name, |
16 | 54 | instance_id=str(instance.pk), |
17 | | - status=ModificationAudit.Status.REQUESTED, |
18 | 55 | ) |
19 | 56 |
|
| 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 | + |
20 | 60 |
|
21 | 61 | class ModificationAudit(BaseAbstractModel): |
22 | 62 | class Action(models.TextChoices): |
@@ -58,41 +98,32 @@ class Meta: |
58 | 98 | def __str__(self) -> str: |
59 | 99 | return str(self.instance) |
60 | 100 |
|
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 | + } |
68 | 107 |
|
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 |
73 | 110 |
|
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 |
80 | 114 |
|
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 |
83 | 118 |
|
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]) |
91 | 122 |
|
92 | | - if save: |
93 | | - self.instance.save() |
| 123 | + return modified_data |
94 | 124 |
|
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) |
96 | 127 |
|
97 | 128 |
|
98 | 129 | class ModificationAuditComment(BaseAbstractModel): |
|
0 commit comments