88primitives = [int , str , unicode , bool , type (None )]
99non_object_types = [dict , list , tuple ]
1010
11+ SENTINEL = '__SENTINEL__'
12+
1113
1214class RestModelBase (type ):
1315 # Called when class is imported got guarantee proper setup of child classes to parent
@@ -104,36 +106,75 @@ def _bind_data(obj, data):
104106 else :
105107 setattr (obj , key , copy .deepcopy (val ))
106108
107- # Converts object structure into JSON
108- def _serialize_data (self , obj ):
109+ @staticmethod
110+ def _get_reference_data (ref , idx ):
111+ ref_type = type (ref )
112+ ret = ref
113+ if ref_type == list :
114+ try :
115+ ret = ref [idx ]
116+ except ValueError :
117+ ret = SENTINEL
118+ elif ref_type == dict :
119+ ret = ref .get (idx , SENTINEL )
120+
121+ return ret
122+
123+ def _serialize_data (self , obj , ref ):
124+ '''Converts the local Python objects into a JSON structure.
125+
126+ There are 3 cases we need to handle in this recursive function:
127+ 1. Primitives: Should be serialized to JSON as-is
128+ 2. List: Iterate over all elements and call _serialize_data on each
129+ 3. Dictionary/Object: Recursively call _serialize_data
130+
131+ Arguments:
132+ obj - The current attribute on the model which we are comparing
133+ against the previous state for changes.
134+ ref - Reference data stored on the `RestModel` for the attribute
135+ layer currently being looked at. Used for saving since a
136+ HTTP PATCH is used for updates, where we only send the delta.
137+ '''
109138 local_diff = {}
110139
111- # Convert to dictionary
140+ # Get the `dict` representation of the `object` to combine cases
112141 if type (obj ) not in (primitives + non_object_types ):
113142 obj = obj .__dict__
114143
115- # For all of the top level keys
144+ # Check each value in the `dict` for changes
116145 for key , value in obj .iteritems ():
117- # Do not worry about private
118- if key .startswith ('_' ):
146+ # 0. Private variables: SKIP
147+ # Escape early if nothing has changed
148+ if key .startswith ('_' ) or self ._get_reference_data (ref , key ) == value :
119149 continue
120150
151+ # Determine what type the current `value` is to process one of the three cases
121152 value_type = type (value )
122- if value_type not in primitives :
123- # Nested structure
153+
154+ # 1. Primitives
155+ if value_type in primitives :
156+ # If the value of the field is not what we've seen before, add it to the diff
157+ if ref != value :
158+ local_diff [key ] = value
159+
160+ # 2/3. Objects
161+ else :
162+ # 2. Lists
124163 if value_type == list :
125- local_diff [key ] = copy .deepcopy (value )
164+ # Process each idex of the `list`
165+ local_diff [key ] = []
126166 for idx , inner_value in enumerate (value ):
127- if type (inner_value ) not in primitives :
128- local_diff [key ][ idx ] = self . _serialize_data (inner_value )
167+ if type (inner_value ) in primitives :
168+ local_diff [key ]. append (inner_value )
129169 else :
130- local_diff [key ][ idx ] = inner_value
131- # Object/Dictionary
170+ local_diff [key ]. append ( self . _serialize_data ( inner_value , self . _get_reference_data ( ref , idx )))
171+ # 3. Object/Dictionary
132172 else :
133- local_diff [key ] = self ._serialize_data (value )
134- # Primitive type
135- elif self ._data .get (key , '__SENTINEL__' ) != value :
136- local_diff [key ] = value
173+ new_ref = self ._get_reference_data (ref , key )
174+ _data = self ._serialize_data (value , new_ref )
175+ if not _data and type (value ) not in (primitives + non_object_types ):
176+ continue
177+ local_diff [key ] = _data
137178
138179 return local_diff
139180
@@ -157,7 +198,7 @@ def get_absolute_url(self):
157198 # Does not save nested keys
158199 def save (self , raise_exception = False , ** kwargs ):
159200 # Difference between the original model and new one
160- diff = self ._serialize_data (self )
201+ diff = self ._serialize_data (self , self . _data )
161202
162203 # Perform a PATCH only if there are difference
163204 if diff :
0 commit comments