11import logging
2+ from datetime import datetime , timedelta
23from decimal import Decimal
34from functools import lru_cache
4- from typing import Any , Dict , List , Optional , Tuple
5+ from typing import Any , Dict , List , Tuple
56
67from django .core .serializers import serialize
8+ from django .core .serializers .json import DjangoJSONEncoder
79from django .db .models import (
810 DateField ,
911 DateTimeField ,
1820 parse_duration ,
1921 parse_time ,
2022)
23+ from django .utils .duration import duration_string
2124
2225import orjson
2326
3235
3336logger = logging .getLogger (__name__ )
3437
38+ django_json_encoder = DjangoJSONEncoder ()
39+
3540
3641class JSONDecodeError (Exception ):
3742 pass
@@ -107,6 +112,69 @@ def _get_many_to_many_field_related_names_from_meta(meta):
107112 return _get_many_to_many_field_related_names_from_meta (model ._meta )
108113
109114
115+ def _get_m2m_field_serialized (model : Model , field_name ) -> List :
116+ pks = []
117+
118+ try :
119+ related_descriptor = getattr (model , field_name )
120+
121+ # Get `pk` from `all` because it will re-use the cached data if the m-2-m field is prefetched
122+ # Using `values_list("pk", flat=True)` or `only()` won't use the cached prefetched values
123+ pks = [m .pk for m in related_descriptor .all ()]
124+ except ValueError :
125+ # ValueError is thrown when the model doesn't have an id already set
126+ pass
127+
128+ return pks
129+
130+
131+ def _handle_inherited_models (model : Model , model_json : Dict ):
132+ """
133+ Handle if the model has a parent (i.e. the model is a subclass of another model).
134+
135+ Subclassed model's fields don't get serialized
136+ (https://docs.djangoproject.com/en/stable/topics/serialization/#inherited-models)
137+ so those fields need to be retrieved manually.
138+ """
139+
140+ if model ._meta .get_parent_list ():
141+ for field in model ._meta .get_fields ():
142+ if (
143+ field .name not in model_json
144+ and hasattr (field , "primary_key" )
145+ and not field .primary_key
146+ ):
147+ if field .is_relation :
148+ # We already serialized the m2m fields above, so we can skip them, but need to handle FKs
149+ if not field .many_to_many :
150+ foreign_key_field = getattr (model , field .name )
151+ foreign_key_field_pk = getattr (
152+ foreign_key_field ,
153+ "pk" ,
154+ getattr (foreign_key_field , "id" , None ),
155+ )
156+ model_json [field .name ] = foreign_key_field_pk
157+ else :
158+ value = getattr (model , field .name )
159+
160+ # Explicitly handle `timedelta`, but use the DjangoJSONEncoder for everything else
161+ if isinstance (value , timedelta ):
162+ value = duration_string (value )
163+ else :
164+ # Make sure the value is properly serialized
165+ value = django_json_encoder .encode (value )
166+
167+ # The DjangoJSONEncoder has extra double-quotes for strings so remove them
168+ if (
169+ isinstance (value , str )
170+ and value .startswith ('"' )
171+ and value .endswith ('"' )
172+ ):
173+ value = value [1 :- 1 ]
174+
175+ model_json [field .name ] = value
176+
177+
110178def _get_model_dict (model : Model ) -> dict :
111179 """
112180 Serializes Django models. Uses the built-in Django JSON serializer, but moves the data around to
@@ -115,27 +183,29 @@ def _get_model_dict(model: Model) -> dict:
115183
116184 _parse_field_values_from_string (model )
117185
118- # Django's `serialize` method always returns an array, so remove the brackets from the resulting string
186+ # Django's `serialize` method always returns a string of an array,
187+ # so remove the brackets from the resulting string
119188 serialized_model = serialize ("json" , [model ])[1 :- 1 ]
189+
190+ # Convert the string into a dictionary and grab the `pk`
120191 model_json = orjson .loads (serialized_model )
121192 model_pk = model_json .get ("pk" )
193+
194+ # Shuffle around the serialized pieces to condense the size of the payload
122195 model_json = model_json .get ("fields" )
123196 model_json ["pk" ] = model_pk
124197
125- for related_name in _get_many_to_many_field_related_names (model ):
126- pks = []
198+ # Set `pk` for models that subclass another model which only have `id` set
199+ if not model_pk :
200+ model_json ["pk" ] = model .pk or model .id
127201
128- try :
129- related_descriptor = getattr (model , related_name )
202+ # Add in m2m fields
203+ m2m_field_names = _get_many_to_many_field_related_names (model )
130204
131- # Get `pk` from `all` because it will re-use the cached data if the m-2-m field is prefetched
132- # Using `values_list("pk", flat=True)` or `only()` won't use the cached prefetched values
133- pks = [m .pk for m in related_descriptor .all ()]
134- except ValueError :
135- # ValueError is thrown when the model doesn't have an id already set
136- pass
205+ for m2m_field_name in m2m_field_names :
206+ model_json [m2m_field_name ] = _get_m2m_field_serialized (model , m2m_field_name )
137207
138- model_json [ related_name ] = pks
208+ _handle_inherited_models ( model , model_json )
139209
140210 return model_json
141211
0 commit comments