1- from copy import deepcopy
2-
31from django .core .exceptions import ValidationError
42from django .db import transaction
53from django .db .models import Q
64from django .utils .translation import gettext_lazy as _
75from rest_framework import serializers
86from swapper import load_model
97
10- from openwisp_users .api .mixins import FilterSerializerByOrgManaged
118from openwisp_utils .api .serializers import ValidatedModelSerializer
129
10+ from ...serializers import BaseSerializer
1311from .. import settings as app_settings
1412
1513Template = load_model ('config' , 'Template' )
@@ -24,10 +22,6 @@ class BaseMeta:
2422 read_only_fields = ['created' , 'modified' ]
2523
2624
27- class BaseSerializer (FilterSerializerByOrgManaged , ValidatedModelSerializer ):
28- pass
29-
30-
3125class TemplateSerializer (BaseSerializer ):
3226 config = serializers .JSONField (initial = {}, required = False )
3327 tags = serializers .StringRelatedField (many = True , read_only = True )
@@ -110,7 +104,12 @@ def get_queryset(self):
110104 return queryset
111105
112106
113- class BaseConfigSerializer (serializers .ModelSerializer ):
107+ class BaseConfigSerializer (ValidatedModelSerializer ):
108+ # The device object is excluded from validation
109+ # because this serializer focuses on validating
110+ # config objects.
111+ exclude_validation = ['device' ]
112+
114113 class Meta :
115114 model = Config
116115 fields = ['status' , 'error_reason' , 'backend' , 'templates' , 'context' , 'config' ]
@@ -119,8 +118,26 @@ class Meta:
119118 'error_reason' : {'read_only' : True },
120119 }
121120
121+ def validate (self , data ):
122+ """
123+ The validation here is a bit tricky:
124+
125+ For existing devices, we have to perform the
126+ model validation of the existing config object,
127+ because if we simulate the validation on a new
128+ config object pointing to an existing device,
129+ the validation will fail because a config object
130+ for this device already exists (due to one-to-one relationship).
131+ """
132+ device = self .context .get ('device' )
133+ if not self .instance and device :
134+ # Existing device with existing config
135+ if device ._has_config ():
136+ self .instance = device .config
137+ return super ().validate (data )
138+
122139
123- class DeviceConfigMixin ( object ):
140+ class DeviceConfigSerializer ( BaseSerializer ):
124141 def _get_config_templates (self , config_data ):
125142 return [template .pk for template in config_data .pop ('templates' , [])]
126143
@@ -131,11 +148,29 @@ def _prepare_config(self, device, config_data):
131148 config .full_clean ()
132149 return config
133150
151+ def _is_config_data_relevant (self , config_data ):
152+ """
153+ Returns True if ``config_data`` does not equal
154+ the default values and hence the config is useful.
155+ """
156+ return not (
157+ config_data .get ('backend' ) == app_settings .DEFAULT_BACKEND
158+ and not config_data .get ('templates' )
159+ and not config_data .get ('context' )
160+ and not config_data .get ('config' )
161+ )
162+
134163 @transaction .atomic
135164 def _create_config (self , device , config_data ):
136165 config_templates = self ._get_config_templates (config_data )
137166 try :
138167 if not device ._has_config ():
168+ # if the user hasn't set any useful config data, skip
169+ if (
170+ not self ._is_config_data_relevant (config_data )
171+ and not config_templates
172+ ):
173+ return
139174 config = Config (device = device , ** config_data )
140175 config .full_clean ()
141176 config .save ()
@@ -151,15 +186,10 @@ def _create_config(self, device, config_data):
151186 raise serializers .ValidationError ({'config' : error .messages })
152187
153188 def _update_config (self , device , config_data ):
154- if (
155- config_data .get ('backend' ) == app_settings .DEFAULT_BACKEND
156- and not config_data .get ('templates' )
157- and not config_data .get ('context' )
158- and not config_data .get ('config' )
159- ):
160- # Do not create Config object if config_data only
161- # contains the default value.
162- # See https://github.com/openwisp/openwisp-controller/issues/699
189+ # Do not create Config object if config_data only
190+ # contains the default values.
191+ # See https://github.com/openwisp/openwisp-controller/issues/699
192+ if not self ._is_config_data_relevant (config_data ):
163193 return
164194 if not device ._has_config ():
165195 return self ._create_config (device , config_data )
@@ -190,9 +220,7 @@ class DeviceListConfigSerializer(BaseConfigSerializer):
190220 templates = FilterTemplatesByOrganization (many = True , write_only = True )
191221
192222
193- class DeviceListSerializer (
194- DeviceConfigMixin , FilterSerializerByOrgManaged , serializers .ModelSerializer
195- ):
223+ class DeviceListSerializer (DeviceConfigSerializer ):
196224 config = DeviceListConfigSerializer (required = False )
197225
198226 class Meta (BaseMeta ):
@@ -219,14 +247,13 @@ class Meta(BaseMeta):
219247 'management_ip' : {'allow_blank' : True },
220248 }
221249
222- def validate (self , attrs ):
223- device_data = deepcopy (attrs )
250+ def validate (self , data ):
224251 # Validation of "config" is performed after
225252 # device object is created in the "create" method.
226- device_data .pop ('config' , None )
227- device = self . instance or self . Meta . model ( ** device_data )
228- device . full_clean ()
229- return attrs
253+ config_data = data .pop ('config' , None )
254+ data = super (). validate ( data )
255+ data [ 'config' ] = config_data
256+ return data
230257
231258 def create (self , validated_data ):
232259 config_data = validated_data .pop ('config' , None )
@@ -247,7 +274,7 @@ class DeviceDetailConfigSerializer(BaseConfigSerializer):
247274 templates = FilterTemplatesByOrganization (many = True )
248275
249276
250- class DeviceDetailSerializer (DeviceConfigMixin , BaseSerializer ):
277+ class DeviceDetailSerializer (DeviceConfigSerializer ):
251278 config = DeviceDetailConfigSerializer (allow_null = True )
252279 is_deactivated = serializers .BooleanField (read_only = True )
253280
@@ -280,7 +307,7 @@ def update(self, instance, validated_data):
280307 if config_data :
281308 self ._update_config (instance , config_data )
282309
283- elif hasattr ( instance , 'config' ) and validated_data .get ('organization' ):
310+ elif instance . _has_config ( ) and validated_data .get ('organization' ):
284311 if instance .organization != validated_data .get ('organization' ):
285312 # config.device.organization is used for validating
286313 # the organization of templates. It is also used for adding
0 commit comments