11import functools
22import typing
33
4+ from admin_api .serializers .socialaccount import EmailAddressNestedAdminSerializer , SocialAccountNestedAdminSerializer
5+ from admin_api .services .socialaccount import delete_social_accounts_and_cleanup_user_emails
6+ from allauth .account .models import EmailAddress
7+ from allauth .socialaccount .models import SocialAccount
48from core .const .account import generate_random_password
59from core .const .serializer import COMMON_ADMIN_FIELDS
610from core .serializer .base_abstract_serializer import BaseAbstractSerializer
711from core .serializer .json_schema_serializer import JsonSchemaSerializer
12+ from core .serializer .nested_model_serializer import NestedFieldModelSerializer , NestedFieldSpec
813from core .serializer .read_only_serializer import ReadOnlyModelSerializer
914from rest_framework import serializers
1015from user .models import UserExt
1116from user .models .organization import Organization
1217
1318
14- class UserAdminSerializer (JsonSchemaSerializer , serializers . ModelSerializer ):
19+ class UserAdminSerializer (JsonSchemaSerializer , NestedFieldModelSerializer ):
1520 str_repr = serializers .CharField (source = "__str__" , read_only = True )
21+ email_addresses = EmailAddressNestedAdminSerializer (many = True , required = False , source = "emailaddress_set" )
22+ social_accounts = SocialAccountNestedAdminSerializer (many = True , required = False , source = "socialaccount_set" )
1623
1724 class Meta :
1825 model = UserExt
@@ -28,17 +35,75 @@ class Meta:
2835 "str_repr" ,
2936 "date_joined" ,
3037 "last_login" ,
38+ "email_addresses" ,
39+ "social_accounts" ,
3140 )
3241 extra_kwargs = {
3342 "id" : {"read_only" : True },
3443 "date_joined" : {"read_only" : True },
3544 "last_login" : {"read_only" : True },
3645 }
46+ nested_fields = {
47+ "emailaddress_set" : NestedFieldSpec (
48+ related_manager_name = "emailaddress_set" ,
49+ child_model = EmailAddress ,
50+ parent_fk_name = "user" ,
51+ ),
52+ "socialaccount_set" : NestedFieldSpec (
53+ related_manager_name = "socialaccount_set" ,
54+ child_model = SocialAccount ,
55+ parent_fk_name = "user" ,
56+ ),
57+ }
58+
59+ def validate (self , attrs : dict ) -> dict :
60+ # social_accounts=[] 는 마지막 SA cascade 를 트리거해 같은 user 의 EA 전체를 삭제함.
61+ # 같은 PATCH 의 email_addresses 입력은 cascade 로 즉시 사라져 의도와 다른 결과가 되므로,
62+ # 실제로 cascade 가 발생하는 경우(기존 SA 존재 + SA=[] + EA 입력 있음)에만 거부.
63+ if (
64+ attrs .get ("socialaccount_set" ) == []
65+ and attrs .get ("emailaddress_set" )
66+ and self .instance is not None
67+ and self .instance .socialaccount_set .all ()
68+ ):
69+ msg = "모든 SocialAccount 를 제거하면 EmailAddress 도 cascade 로 삭제됩니다 — 같은 PATCH 에서 EmailAddress 를 함께 변경할 수 없습니다."
70+ raise serializers .ValidationError (msg )
71+ return attrs
72+
73+ def validate_social_accounts (self , value : list [dict ]) -> list [dict ]:
74+ # SocialAccount는 nested에서 delete-only — 모든 입력 id 가 이 유저의 기존 SA 와 매칭돼야 함.
75+ # PATCH(partial=True) 에서는 nested 의 required 가 풀려 id 가 없을 수 있음 — 명시적으로 거부.
76+ if any ("id" not in item for item in value ):
77+ raise serializers .ValidationError ("SocialAccount는 nested API에서 생성할 수 없습니다 (id 필수)." )
78+ provided_ids = {item ["id" ] for item in value }
79+ # UserAdminViewSet 의 prefetch_related 캐시 활용.
80+ existing_ids = {sa .id for sa in self .instance .socialaccount_set .all ()} if self .instance else set ()
81+ if unknown := provided_ids - existing_ids :
82+ msg = f"존재하지 않거나 이 유저의 것이 아닌 SocialAccount id: { sorted (map (str , unknown ))} "
83+ raise serializers .ValidationError (msg )
84+ return value
3785
3886 def create (self , validated_data : dict [str , typing .Any ]) -> UserExt :
3987 password = generate_random_password ()
4088 self ._generated_password = password
41- return UserExt .objects .create_user (** validated_data , password = password )
89+ nested_data = {k : validated_data .pop (k , []) or [] for k in self .Meta .nested_fields }
90+ instance = UserExt .objects .create_user (** validated_data , password = password )
91+ self ._apply_nested_sync (instance , nested_data )
92+ return instance
93+
94+ def _apply_nested_sync (self , instance : UserExt , nested_data : dict [str , list [dict ] | None ]) -> None :
95+ # SocialAccount는 delete-only — 기존 set 에서 input 에 없는 것만 삭제.
96+ sa_data = nested_data .pop ("socialaccount_set" , None )
97+ super ()._apply_nested_sync (instance , nested_data )
98+
99+ if sa_data is None :
100+ return
101+
102+ provided_ids = {item ["id" ] for item in sa_data }
103+ # prefetch_related 캐시 활용.
104+ existing_ids = {sa .id for sa in instance .socialaccount_set .all ()}
105+ if to_delete_ids := existing_ids - provided_ids :
106+ delete_social_accounts_and_cleanup_user_emails (SocialAccount .objects .filter (id__in = to_delete_ids ))
42107
43108
44109class UserAdminSignInSerializerData (typing .TypedDict ):
0 commit comments