22import logging
33import traceback
44from abc import ABC
5- from typing import Any
5+ from collections .abc import Sequence
6+ from typing import Any , ClassVar , Optional
67
78import django .db .models
89import graphene
2223logger = logging .getLogger (__name__ )
2324
2425
26+ def _require_io_setting (mutation_cls : type , name : str ) -> Any :
27+ """Raise ``NotImplementedError`` if ``cls.IOSettings.<name>`` is missing or ``None``."""
28+ io_settings = getattr (mutation_cls , "IOSettings" , None )
29+ if io_settings is None :
30+ raise NotImplementedError (
31+ f"{ mutation_cls .__name__ } must define an IOSettings class."
32+ )
33+ value = getattr (io_settings , name , None )
34+ if value is None :
35+ raise NotImplementedError (
36+ f"{ mutation_cls .__name__ } .IOSettings.{ name } must be set by the "
37+ f"subclass."
38+ )
39+ return value
40+
41+
2542class OpenContractsNode (Node ):
2643 class Meta :
2744 name = "Node"
@@ -75,7 +92,8 @@ def resolve_total_count(root, info, **kwargs) -> Any:
7592
7693class DRFDeletion (graphene .Mutation ):
7794 class IOSettings (ABC ):
78- lookup_field = "id"
95+ lookup_field : ClassVar [str ] = "id"
96+ model : ClassVar [Optional [type [django .db .models .Model ]]] = None
7997
8098 class Arguments :
8199 id = graphene .String (required = False )
@@ -88,12 +106,24 @@ class Arguments:
88106 @graphql_ratelimit (rate = RateLimits .WRITE_LIGHT )
89107 def mutate (cls , root , info , * args , ** kwargs ) -> "DRFDeletion" :
90108
109+ # Unlike ``DRFMutation.mutate`` below, this method intentionally does
110+ # NOT wrap the body in ``except Exception``. Errors (including the
111+ # ``NotImplementedError`` from ``_require_io_setting`` and the
112+ # ``ValueError`` for missing lookup args) propagate to the GraphQL
113+ # framework as raw execution errors.
91114 ok = False
92115
93- id = from_global_id (kwargs .get (cls .IOSettings .lookup_field , None ))[1 ]
116+ model = _require_io_setting (cls , "model" )
117+ lookup_field = cls .IOSettings .lookup_field
118+ lookup_value = kwargs .get (lookup_field )
119+ if lookup_value is None :
120+ raise ValueError (
121+ f"'{ lookup_field } ' is required to identify the object to delete."
122+ )
123+ id = from_global_id (lookup_value )[1 ]
94124 # Filter through visible_to_user() to prevent IDOR -- returns same
95125 # DoesNotExist error whether object is missing or user lacks access.
96- obj = cls . IOSettings . model .objects .visible_to_user (info .context .user ).get (pk = id )
126+ obj = model .objects .visible_to_user (info .context .user ).get (pk = id )
97127
98128 # if there's a user lock, only the lock holder (or superuser) can proceed
99129 if hasattr (obj , "user_lock" ) and obj .user_lock is not None :
@@ -126,11 +156,13 @@ def mutate(cls, root, info, *args, **kwargs) -> "DRFDeletion":
126156
127157class DRFMutation (graphene .Mutation ):
128158 class IOSettings (ABC ):
129- pk_fields : list [str | int ] = []
130- lookup_field = "id"
131- model : django .db .models .Model = None
132- graphene_model : DjangoObjectType = None
133- serializer = None
159+ # Frozen default — subclasses override with their own list/tuple.
160+ # Using a tuple here avoids the shared-mutable-default footgun.
161+ pk_fields : ClassVar [Sequence [str ]] = ()
162+ lookup_field : ClassVar [str ] = "id"
163+ model : ClassVar [Optional [type [django .db .models .Model ]]] = None
164+ graphene_model : ClassVar [Optional [type [DjangoObjectType ]]] = None
165+ serializer : ClassVar [Optional [type [serializers .Serializer ]]] = None
134166
135167 class Arguments :
136168 pass
@@ -178,20 +210,25 @@ def mutate(cls, root, info, *args, **kwargs) -> "DRFMutation":
178210 raise ValueError ("No user in this request..." )
179211
180212 logger .info (f"DRFMutation - kwargs: { kwargs } " )
181- serializer = cls .IOSettings .serializer
213+ serializer = _require_io_setting (cls , "serializer" )
214+ model = _require_io_setting (cls , "model" )
215+ # ``graphene_model`` is the class itself; ``__name__`` is the GraphQL
216+ # type name (e.g. ``"CorpusType"``). ``__class__.__name__`` would
217+ # return the metaclass name (``"SubclassWithMeta_Meta"``) which is
218+ # the bug this PR fixes — kept dereferenced as ``__name__`` below.
219+ graphene_model = _require_io_setting (cls , "graphene_model" )
182220
183221 if hasattr (cls .IOSettings , "pk_fields" ):
184222 for pk_field in cls .IOSettings .pk_fields :
185223 if pk_field in kwargs :
186- if isinstance ( kwargs [pk_field ], list ):
187- pk_value = []
188- for global_id in kwargs [pk_field ]:
189- # global_id is already the ID string, not a key
190- pk_value . append ( from_global_id ( global_id )[ 1 ])
224+ raw_value = kwargs [pk_field ]
225+ if isinstance ( raw_value , list ):
226+ kwargs [pk_field ] = [
227+ from_global_id ( global_id )[ 1 ] for global_id in raw_value
228+ ]
191229 else :
192- logger .info (f"pk field is: { kwargs .get (pk_field , None )} " )
193- pk_value = from_global_id (kwargs .get (pk_field , None ))[1 ]
194- kwargs [pk_field ] = pk_value
230+ logger .info (f"pk field is: { raw_value } " )
231+ kwargs [pk_field ] = from_global_id (raw_value )[1 ]
195232
196233 # Check if lookup_field exists in IOSettings and if it's in kwargs
197234 # This allows create mutations to work without requiring lookup_field
@@ -204,10 +241,8 @@ def mutate(cls, root, info, *args, **kwargs) -> "DRFMutation":
204241 logger .info ("Lookup_field specified - update" )
205242 # Filter through visible_to_user() to prevent IDOR --
206243 # returns same DoesNotExist whether missing or no access.
207- obj = cls .IOSettings .model .objects .visible_to_user (
208- info .context .user
209- ).get (
210- pk = from_global_id (kwargs .get (cls .IOSettings .lookup_field , None ))[1 ]
244+ obj = model .objects .visible_to_user (info .context .user ).get (
245+ pk = from_global_id (kwargs [cls .IOSettings .lookup_field ])[1 ]
211246 )
212247
213248 logger .info (f"Retrieved obj: { obj } " )
@@ -243,9 +278,7 @@ def mutate(cls, root, info, *args, **kwargs) -> "DRFMutation":
243278 obj_serializer .save ()
244279 ok = True
245280 message = "Success"
246- obj_id = to_global_id (
247- cls .IOSettings .graphene_model .__class__ .__name__ , obj .id
248- )
281+ obj_id = to_global_id (graphene_model .__name__ , obj .id )
249282 logger .info ("Succeeded updating obj" )
250283
251284 else :
@@ -265,9 +298,7 @@ def mutate(cls, root, info, *args, **kwargs) -> "DRFMutation":
265298
266299 ok = True
267300 message = "Success"
268- obj_id = to_global_id (
269- cls .IOSettings .graphene_model .__class__ .__name__ , obj .id
270- )
301+ obj_id = to_global_id (graphene_model .__name__ , obj .id )
271302
272303 except serializers .ValidationError as ve :
273304 logger .warning (f"Validation error in mutation: { ve .detail } " )
0 commit comments