Skip to content

Commit d299d9e

Browse files
committed
feat: FK/M2M 필드의 choices를 별도 API로 분리
1 parent e660b36 commit d299d9e

1 file changed

Lines changed: 70 additions & 18 deletions

File tree

app/core/viewset/json_schema_viewset.py

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

3-
import functools
43
import typing
54

65
from core.const.tag import OpenAPITag
@@ -23,26 +22,74 @@ def __new__(cls, *args: tuple, **kwargs: dict) -> JsonSchemaViewSet:
2322
return super().__new__(cls)
2423

2524
@staticmethod
26-
@functools.lru_cache
27-
def get_enum_values(model_qs: QuerySet, is_nullable: bool) -> list[dict[str, str]]:
28-
enum_values: list[dict[str, str]] = [{"const": None, "title": "빈 값"}] if is_nullable else []
29-
30-
qs = model_qs.all()
31-
if hasattr(qs, "filter_active"):
32-
qs = qs.filter_active()
33-
elif hasattr(model_qs.model, "is_active"):
34-
qs = qs.filter(is_active=True)
25+
def _get_choices_from_queryset(qs: QuerySet, is_nullable: bool) -> list[dict[str, str]]:
26+
choices: list[dict[str, str]] = [{"const": None, "title": "빈 값"}] if is_nullable else []
27+
28+
related_model = qs.model
29+
if hasattr(related_model, "get_choices_queryset"):
30+
qs = related_model.get_choices_queryset()
31+
else:
32+
qs = qs.all()
33+
if hasattr(qs, "filter_active"):
34+
qs = qs.filter_active()
35+
elif hasattr(related_model, "is_active"):
36+
qs = qs.filter(is_active=True)
3537

3638
for row in qs:
37-
enum_values.append({"const": str(row.pk), "title": str(row)})
39+
choices.append({"const": str(row.pk), "title": str(row)})
3840

39-
return enum_values
41+
return choices
4042

4143
@staticmethod
4244
def set_ui_schema(ui_schema: dict, field_name: str, data: dict) -> None:
4345
ui_schema.setdefault(field_name, {})
4446
ui_schema[field_name].update(data)
4547

48+
def _get_related_field_info(self) -> list[tuple[str, object, serializers.Field, bool]]:
49+
"""Returns list of (field_name, model_field, serializer_field, is_m2m) for FK/M2M fields."""
50+
serializer_class = typing.cast(type[JsonSchemaSerializer], self.get_serializer_class())
51+
52+
if not hasattr(serializer_class.Meta, "model"):
53+
return []
54+
55+
ser_fields: dict[str, serializers.Field] = serializer_class().fields
56+
model_fields = serializer_class.Meta.model._meta.fields
57+
model_m2m_fields = serializer_class.Meta.model._meta.many_to_many
58+
schema = serializer_class.get_json_schema()
59+
60+
result = []
61+
for field in model_fields + model_m2m_fields:
62+
if field.name not in schema.get("properties", {}) or field.name not in ser_fields:
63+
continue
64+
65+
serializer_field = ser_fields[field.name]
66+
67+
if isinstance(field, ForeignKey):
68+
s_field = typing.cast(serializers.PrimaryKeyRelatedField | None, serializer_field)
69+
if not s_field or serializer_field.read_only:
70+
continue
71+
result.append((field.name, field, serializer_field, False))
72+
elif isinstance(field, ManyToManyField):
73+
s_field = typing.cast(serializers.ManyRelatedField | None, serializer_field)
74+
if not s_field or serializer_field.read_only:
75+
continue
76+
result.append((field.name, field, serializer_field, True))
77+
78+
return result
79+
80+
def get_choices(self) -> dict[str, list[dict[str, str]]]:
81+
choices: dict[str, list[dict[str, str]]] = {}
82+
83+
for field_name, field, serializer_field, is_m2m in self._get_related_field_info():
84+
if is_m2m:
85+
qs = typing.cast(serializers.ManyRelatedField, serializer_field).child_relation.get_queryset()
86+
choices[field_name] = self._get_choices_from_queryset(qs, False)
87+
else:
88+
qs = typing.cast(serializers.PrimaryKeyRelatedField, serializer_field).get_queryset()
89+
choices[field_name] = self._get_choices_from_queryset(qs, field.null)
90+
91+
return choices
92+
4693
def get_json_schema(self) -> dict: # noqa: C901
4794
serializer_class = typing.cast(type[JsonSchemaSerializer], self.get_serializer_class())
4895

@@ -70,19 +117,15 @@ def get_json_schema(self) -> dict: # noqa: C901
70117
serializer_field = ser_fields[field.name]
71118

72119
if isinstance(field, ForeignKey):
73-
if not (s_field := typing.cast(serializers.PrimaryKeyRelatedField | None, serializer_field)):
120+
if not typing.cast(serializers.PrimaryKeyRelatedField | None, serializer_field):
74121
continue
75122
if serializer_field.read_only:
76123
continue
77-
e_values = self.get_enum_values(s_field.get_queryset(), field.null)
78-
result["schema"]["properties"][field.name]["oneOf"] = e_values
79124
elif isinstance(field, ManyToManyField):
80-
if not (s_field := typing.cast(serializers.ManyRelatedField | None, serializer_field)):
125+
if not typing.cast(serializers.ManyRelatedField | None, serializer_field):
81126
continue
82127
if serializer_field.read_only:
83128
continue
84-
e_values = self.get_enum_values(s_field.child_relation.get_queryset(), False)
85-
result["schema"]["properties"][field.name]["items"]["oneOf"] = e_values
86129
result["schema"]["properties"][field.name]["uniqueItems"] = True
87130
self.set_ui_schema(result["ui_schema"], field.name, {"ui:field": "m2m_select"})
88131
elif isinstance(field, FileField):
@@ -115,3 +158,12 @@ def get_json_schema(self) -> dict: # noqa: C901
115158
@decorators.action(detail=False, methods=["get"], url_path="json-schema")
116159
def response_json_schema(self, *args: tuple, **kwargs: dict) -> response.Response:
117160
return response.Response(data=self.get_json_schema())
161+
162+
@utils.extend_schema(
163+
tags=[OpenAPITag.ADMIN_JSON_SCHEMA],
164+
summary="Choices for related fields",
165+
responses={status.HTTP_200_OK: openapi.OpenApiResponse(response=types.OpenApiTypes.OBJECT)},
166+
)
167+
@decorators.action(detail=False, methods=["get"], url_path="choices")
168+
def response_choices(self, *args: tuple, **kwargs: dict) -> response.Response:
169+
return response.Response(data=self.get_choices())

0 commit comments

Comments
 (0)