3
3
"""Diode NetBox Plugin - API - Differ."""
4
4
5
5
import copy
6
+ import datetime
6
7
import logging
7
8
8
9
from django .contrib .contenttypes .models import ContentType
12
13
from .common import Change , ChangeSet , ChangeSetException , ChangeSetResult , ChangeType
13
14
from .plugin_utils import get_primary_value , legal_fields
14
15
from .supported_models import extract_supported_models
15
- from .transformer import cleanup_unresolved_references , transform_proto_json
16
+ from .transformer import cleanup_unresolved_references , set_custom_field_defaults , transform_proto_json
16
17
17
18
logger = logging .getLogger (__name__ )
18
19
@@ -68,9 +69,31 @@ def prechange_data_from_instance(instance) -> dict: # noqa: C901
68
69
else :
69
70
prechange_data [field_name ] = value
70
71
72
+ if hasattr (instance , "get_custom_fields" ):
73
+ custom_field_values = instance .get_custom_fields ()
74
+ cfmap = {}
75
+ for cf , value in custom_field_values .items ():
76
+ if isinstance (value , (datetime .datetime , datetime .date )):
77
+ cfmap [cf .name ] = value
78
+ else :
79
+ cfmap [cf .name ] = cf .serialize (value )
80
+ prechange_data ["custom_fields" ] = cfmap
81
+
71
82
return prechange_data
72
83
73
84
85
+ def _harmonize_formats (prechange_data : dict , postchange_data : dict ):
86
+ for k , v in prechange_data .items ():
87
+ if isinstance (v , datetime .datetime ):
88
+ prechange_data [k ] = v .strftime ("%Y-%m-%dT%H:%M:%SZ" )
89
+ elif isinstance (v , datetime .date ):
90
+ prechange_data [k ] = v .strftime ("%Y-%m-%d" )
91
+ elif isinstance (v , int ) and k in postchange_data :
92
+ postchange_data [k ] = int (postchange_data [k ])
93
+ elif isinstance (v , dict ):
94
+ _harmonize_formats (v , postchange_data .get (k , {}))
95
+
96
+
74
97
def clean_diff_data (data : dict , exclude_empty_values : bool = True ) -> dict :
75
98
"""Clean diff data by removing null values."""
76
99
result = {}
@@ -80,8 +103,10 @@ def clean_diff_data(data: dict, exclude_empty_values: bool = True) -> dict:
80
103
continue
81
104
if isinstance (v , list ) and len (v ) == 0 :
82
105
continue
83
- if isinstance (v , dict ) and len (v ) == 0 :
84
- continue
106
+ if isinstance (v , dict ):
107
+ if len (v ) == 0 :
108
+ continue
109
+ v = clean_diff_data (v , exclude_empty_values )
85
110
if isinstance (v , str ) and v == "" :
86
111
continue
87
112
result [k ] = v
@@ -100,7 +125,7 @@ def diff_to_change(
100
125
if change_type == ChangeType .UPDATE and not len (changed_attrs ) > 0 :
101
126
change_type = ChangeType .NOOP
102
127
103
- primary_value = get_primary_value (prechange_data | postchange_data , object_type )
128
+ primary_value = str ( get_primary_value (prechange_data | postchange_data , object_type ) )
104
129
if primary_value is None :
105
130
primary_value = "(unnamed)"
106
131
@@ -111,6 +136,8 @@ def diff_to_change(
111
136
112
137
change = Change (
113
138
change_type = change_type ,
139
+ before = _tidy (prechange_data ),
140
+ data = {},
114
141
object_type = object_type ,
115
142
object_id = prior_id if isinstance (prior_id , int ) else None ,
116
143
ref_id = ref_id ,
@@ -119,17 +146,13 @@ def diff_to_change(
119
146
)
120
147
121
148
if change_type != ChangeType .NOOP :
122
- postchange_data_clean = clean_diff_data (postchange_data )
123
- change .data = sort_dict_recursively (postchange_data_clean )
124
- else :
125
- change .data = {}
126
-
127
- if change_type == ChangeType .UPDATE or change_type == ChangeType .NOOP :
128
- prechange_data_clean = clean_diff_data (prechange_data )
129
- change .before = sort_dict_recursively (prechange_data_clean )
149
+ change .data = _tidy (postchange_data )
130
150
131
151
return change
132
152
153
+ def _tidy (data : dict ) -> dict :
154
+ return sort_dict_recursively (clean_diff_data (data ))
155
+
133
156
def sort_dict_recursively (d ):
134
157
"""Recursively sorts a dictionary by keys."""
135
158
if isinstance (d , dict ):
@@ -161,7 +184,11 @@ def generate_changeset(entity: dict, object_type: str) -> ChangeSetResult:
161
184
# prior state is a model instance
162
185
else :
163
186
prechange_data = prechange_data_from_instance (instance )
164
-
187
+ # merge the prior state that we don't want to overwrite with the new state
188
+ # this is also important for custom fields because they do not appear to
189
+ # respsect paritial update serialization.
190
+ entity = _partially_merge (prechange_data , entity , instance )
191
+ _harmonize_formats (prechange_data , entity )
165
192
changed_data = shallow_compare_dict (
166
193
prechange_data , entity ,
167
194
)
@@ -187,7 +214,25 @@ def generate_changeset(entity: dict, object_type: str) -> ChangeSetResult:
187
214
if errors := change_set .validate ():
188
215
raise ChangeSetException ("Invalid change set" , errors )
189
216
190
- return ChangeSetResult (
217
+
218
+ cs = ChangeSetResult (
191
219
id = change_set .id ,
192
220
change_set = change_set ,
193
221
)
222
+ return cs
223
+
224
+ def _partially_merge (prechange_data : dict , postchange_data : dict , instance ) -> dict :
225
+ """Merge lists and custom_fields rather than replacing the full value..."""
226
+ result = {}
227
+ for key , value in postchange_data .items ():
228
+ # TODO: partially merge lists like tags? all lists?
229
+ result [key ] = value
230
+
231
+ # these are fully merged in from the prechange state because
232
+ # they don't respect partial update serialization.
233
+ if "custom_fields" in postchange_data :
234
+ for key , value in prechange_data .get ("custom_fields" , {}).items ():
235
+ if value is not None and key not in postchange_data ["custom_fields" ]:
236
+ result ["custom_fields" ][key ] = value
237
+ set_custom_field_defaults (result , instance )
238
+ return result
0 commit comments