33
44from api_v2 import models
55
6+ class GameContentSerializer (serializers .HyperlinkedModelSerializer ):
7+ """
8+ Much of the logic included in the GameContentSerializer is intended to
9+ support manipulating data returned by the serializer via query parameters.
10+ """
11+
12+ def remove_unwanted_fields (self , dynamic_params ):
13+ """
14+ Takes the value of the 'fields', a string of comma-separated values,
15+ and removes all fields not in this list from the serializer
16+ """
17+ if fields_to_keep := dynamic_params .pop ("fields" , None ):
18+ fields_to_keep = set (fields_to_keep .split ("," ))
19+ all_fields = set (self .fields .keys ())
20+ for field in all_fields - fields_to_keep :
21+ self .fields .pop (field , None )
622
7- class GameContentSerializer (serializers .HyperlinkedModelSerializer ):
23+ def get_or_create_dynamic_params (self , child ):
24+ """
25+ Creates dynamic params on the serializer context if it doesn't already
26+ exist, then returns the dynamic parameters
27+ """
28+ if "dynamic_params" not in self .fields [child ]._context :
29+ self .fields [child ]._context .update ({"dynamic_params" : {}})
30+ return self .fields [child ]._context ["dynamic_params" ]
31+
32+ @staticmethod
33+ def split_param (dynamic_param ):
34+ """
35+ Splits a dynamic parameter into its target child serializer and value.
36+ Returns the values as a tuple.
37+ eg.
38+ 'document__fields=name' -> ('document', 'fields=name')
39+ 'document__gamesystem__fields=name' -> ('document', 'gamesystem__fields=name')
40+ """
41+ crumbs = dynamic_param .split ("__" )
42+ return crumbs [0 ], "__" .join (crumbs [1 :]) if len (crumbs ) > 1 else None
43+
44+ def set_dynamic_params_for_children (self , dynamic_params ):
45+ """
46+ Passes nested dynamic params to child serializer.
47+ eg. the param 'document__fields=name'
48+ """
49+ for param , fields in dynamic_params .items ():
50+ child , child_dynamic_param = self .split_param (param )
51+ if child in self .fields .keys ():
52+ # Get dynamic parameters for child serializer and update
53+ child_dynamic_params = self .get_or_create_dynamic_params (child )
54+ child_dynamic_params .update ({child_dynamic_param : fields })
55+
56+ # Overwrite existing params to remove 'fields' inherited from parent serializer
57+ self .fields [child ]._context ['dynamic_params' ] = {
58+ ** child_dynamic_params ,
59+ }
60+
61+ @staticmethod
62+ def is_param_dynamic (p ):
63+ """
64+ Returns true if parameter 'p' is a dynamic parameter. Currently the
65+ only dynamic param supported is 'fields', so we check for that
66+ """
67+ return p .endswith ("fields" )
68+
69+ def get_dynamic_params_for_root (self , request ):
70+ """
71+ Returns a dict of dynamic query parameters extracted from the 'request'
72+ object. Only works on the root serializer (child serializers do no
73+ include a 'request' field)
74+ """
75+ query_params = request .query_params .items ()
76+ return {k : v for k , v in query_params if self .is_param_dynamic (k )}
77+
78+ def get_dynamic_params (self ):
79+ """
80+ Returns dynamic parameters stored on the serializer context
81+ """
82+ # The context for ListSerializers is stored on the parent
83+ if isinstance (self .parent , serializers .ListSerializer ):
84+ return self .parent ._context .get ("dynamic_params" , {})
85+ return self ._context .get ("dynamic_params" , {})
86+
87+ def handle_depth_serialization (self , instance , representation ):
88+ """
89+ Handles the serialization of fields based on the current depth
90+ compared to the maximum allowed depth. This function modifies
91+ the representation to include only URLs for nested serializers
92+ when the maximum depth is reached.
93+ """
94+ max_depth = self ._context .get ("max_depth" , 0 )
95+ current_depth = self ._context .get ("current_depth" , 0 )
96+
97+ # if we reach the maximum depth, nested serializers return their pk (url)
98+ if current_depth >= max_depth :
99+ for field_name , field in self .fields .items ():
100+ if isinstance (field , serializers .HyperlinkedModelSerializer ):
101+ nested_representation = representation .get (field_name )
102+ if nested_representation and "url" in nested_representation :
103+ representation [field_name ] = nested_representation ["url" ]
104+ return representation
105+
106+ # otherwise, pass depth to children serializers
107+ for field_name , field in self .fields .items ():
108+ # Guard clause: make sure the child is a GameContentSerializer
109+ if not isinstance (field , GameContentSerializer ):
110+ continue
111+
112+ nested_instance = getattr (instance , field_name )
113+ nested_serializer = field .__class__ (nested_instance , context = {
114+ ** self ._context ,
115+ "current_depth" : current_depth + 1 ,
116+ "max_depth" : max_depth ,
117+ })
118+
119+ # Ensure dynamic params are specific to the child serializer
120+ child_dynamic_params = self .get_or_create_dynamic_params (field_name )
121+ nested_serializer ._context ['dynamic_params' ] = child_dynamic_params
122+ representation [field_name ] = nested_serializer .data
123+ return representation
124+
8125
9- # Adding dynamic "fields" qs parameter.
10126 def __init__ (self , * args , ** kwargs ):
11- # Add default fields variable.
12-
13- # Instantiate the superclass normally
14- super (GameContentSerializer , self ).__init__ (* args , ** kwargs )
15-
16- # The request doesn't exist when generating an OAS file, so we have to check that first
17- if self .context ['request' ]:
18- fields = self .context ['request' ].query_params .get ('fields' )
19- if fields :
20- fields = fields .split (',' )
21- # Drop any fields that are not specified in the `fields` argument.
22- allowed = set (fields )
23- existing = set (self .fields .keys ())
24- for field_name in existing - allowed :
25- self .fields .pop (field_name )
26-
27- depth = self .context ['request' ].query_params .get ('depth' )
28- if depth :
29- try :
30- depth_value = int (depth )
31- if depth_value > 0 and depth_value < 3 :
32- # This value going above 1 could cause performance issues.
33- # Limited to 1 and 2 for now.
34- self .Meta .depth = depth_value
35- # Depth does not reset by default on subsequent requests with malformed urls.
36- else :
37- self .Meta .depth = 0
38- except ValueError :
39- pass # it was not castable to an int.
40- else :
41- self .Meta .depth = 0 #The default.
127+ request = kwargs .get ("context" , {}).get ("request" )
128+ super ().__init__ (* args , ** kwargs )
129+
130+ if request :
131+ self ._context ["max_depth" ] = int (request .query_params .get ("depth" , 0 ))
132+ dynamic_params = self .get_dynamic_params_for_root (request )
133+ self ._context .update ({"dynamic_params" : dynamic_params })
134+
135+ def to_representation (self , instance ):
136+ if dynamic_params := self .get_dynamic_params ().copy ():
137+ self .remove_unwanted_fields (dynamic_params )
138+ self .set_dynamic_params_for_children (dynamic_params )
139+
140+ representation = super ().to_representation (instance )
141+
142+ representation = self .handle_depth_serialization (instance , representation )
143+
144+ return representation
145+
42146
43147 class Meta :
44- abstract = True
148+ abstract = True
0 commit comments