1- from copy import deepcopy
2-
31from django .core .exceptions import ValidationError
42from django .db import transaction
53from django .db .models import Q
86from reversion .models import Version
97from swapper import load_model
108
11- from openwisp_users .api .mixins import FilterSerializerByOrgManaged
129from openwisp_utils .api .serializers import ValidatedModelSerializer
1310
11+ from ...serializers import BaseSerializer
1412from .. import settings as app_settings
1513
1614Template = load_model ('config' , 'Template' )
@@ -25,10 +23,6 @@ class BaseMeta:
2523 read_only_fields = ['created' , 'modified' ]
2624
2725
28- class BaseSerializer (FilterSerializerByOrgManaged , ValidatedModelSerializer ):
29- pass
30-
31-
3226class TemplateSerializer (BaseSerializer ):
3327 config = serializers .JSONField (initial = {}, required = False )
3428 tags = serializers .StringRelatedField (many = True , read_only = True )
@@ -111,7 +105,12 @@ def get_queryset(self):
111105 return queryset
112106
113107
114- class BaseConfigSerializer (serializers .ModelSerializer ):
108+ class BaseConfigSerializer (ValidatedModelSerializer ):
109+ # The device object is excluded from validation
110+ # because this serializer focuses on validating
111+ # config objects.
112+ exclude_validation = ['device' ]
113+
115114 class Meta :
116115 model = Config
117116 fields = ['status' , 'error_reason' , 'backend' , 'templates' , 'context' , 'config' ]
@@ -120,8 +119,26 @@ class Meta:
120119 'error_reason' : {'read_only' : True },
121120 }
122121
122+ def validate (self , data ):
123+ """
124+ The validation here is a bit tricky:
125+
126+ For existing devices, we have to perform the
127+ model validation of the existing config object,
128+ because if we simulate the validation on a new
129+ config object pointing to an existing device,
130+ the validation will fail because a config object
131+ for this device already exists (due to one-to-one relationship).
132+ """
133+ device = self .context .get ('device' )
134+ if not self .instance and device :
135+ # Existing device with existing config
136+ if device ._has_config ():
137+ self .instance = device .config
138+ return super ().validate (data )
139+
123140
124- class DeviceConfigMixin ( object ):
141+ class DeviceConfigSerializer ( BaseSerializer ):
125142 def _get_config_templates (self , config_data ):
126143 return [template .pk for template in config_data .pop ('templates' , [])]
127144
@@ -132,11 +149,29 @@ def _prepare_config(self, device, config_data):
132149 config .full_clean ()
133150 return config
134151
152+ def _is_config_data_relevant (self , config_data ):
153+ """
154+ Returns True if ``config_data`` does not equal
155+ the default values and hence the config is useful.
156+ """
157+ return not (
158+ config_data .get ('backend' ) == app_settings .DEFAULT_BACKEND
159+ and not config_data .get ('templates' )
160+ and not config_data .get ('context' )
161+ and not config_data .get ('config' )
162+ )
163+
135164 @transaction .atomic
136165 def _create_config (self , device , config_data ):
137166 config_templates = self ._get_config_templates (config_data )
138167 try :
139168 if not device ._has_config ():
169+ # if the user hasn't set any useful config data, skip
170+ if (
171+ not self ._is_config_data_relevant (config_data )
172+ and not config_templates
173+ ):
174+ return
140175 config = Config (device = device , ** config_data )
141176 config .full_clean ()
142177 config .save ()
@@ -152,15 +187,10 @@ def _create_config(self, device, config_data):
152187 raise serializers .ValidationError ({'config' : error .messages })
153188
154189 def _update_config (self , device , config_data ):
155- if (
156- config_data .get ('backend' ) == app_settings .DEFAULT_BACKEND
157- and not config_data .get ('templates' )
158- and not config_data .get ('context' )
159- and not config_data .get ('config' )
160- ):
161- # Do not create Config object if config_data only
162- # contains the default value.
163- # See https://github.com/openwisp/openwisp-controller/issues/699
190+ # Do not create Config object if config_data only
191+ # contains the default values.
192+ # See https://github.com/openwisp/openwisp-controller/issues/699
193+ if not self ._is_config_data_relevant (config_data ):
164194 return
165195 if not device ._has_config ():
166196 return self ._create_config (device , config_data )
@@ -191,9 +221,7 @@ class DeviceListConfigSerializer(BaseConfigSerializer):
191221 templates = FilterTemplatesByOrganization (many = True , write_only = True )
192222
193223
194- class DeviceListSerializer (
195- DeviceConfigMixin , FilterSerializerByOrgManaged , serializers .ModelSerializer
196- ):
224+ class DeviceListSerializer (DeviceConfigSerializer ):
197225 config = DeviceListConfigSerializer (required = False )
198226
199227 class Meta (BaseMeta ):
@@ -220,14 +248,13 @@ class Meta(BaseMeta):
220248 'management_ip' : {'allow_blank' : True },
221249 }
222250
223- def validate (self , attrs ):
224- device_data = deepcopy (attrs )
251+ def validate (self , data ):
225252 # Validation of "config" is performed after
226253 # device object is created in the "create" method.
227- device_data .pop ('config' , None )
228- device = self . instance or self . Meta . model ( ** device_data )
229- device . full_clean ()
230- return attrs
254+ config_data = data .pop ('config' , None )
255+ data = super (). validate ( data )
256+ data [ 'config' ] = config_data
257+ return data
231258
232259 def create (self , validated_data ):
233260 config_data = validated_data .pop ('config' , None )
@@ -248,7 +275,7 @@ class DeviceDetailConfigSerializer(BaseConfigSerializer):
248275 templates = FilterTemplatesByOrganization (many = True )
249276
250277
251- class DeviceDetailSerializer (DeviceConfigMixin , BaseSerializer ):
278+ class DeviceDetailSerializer (DeviceConfigSerializer ):
252279 config = DeviceDetailConfigSerializer (allow_null = True )
253280 is_deactivated = serializers .BooleanField (read_only = True )
254281
@@ -281,7 +308,7 @@ def update(self, instance, validated_data):
281308 if config_data :
282309 self ._update_config (instance , config_data )
283310
284- elif hasattr ( instance , 'config' ) and validated_data .get ('organization' ):
311+ elif instance . _has_config ( ) and validated_data .get ('organization' ):
285312 if instance .organization != validated_data .get ('organization' ):
286313 # config.device.organization is used for validating
287314 # the organization of templates. It is also used for adding
0 commit comments