11from __future__ import annotations
22
3- import functools
43import typing
54
65from 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