mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 11:37:21 -06:00
Check for extraneous custom field data on clean()
This commit is contained in:
parent
aed25fea3a
commit
4a8a1ce45c
@ -155,6 +155,8 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
|
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# Validate that termination A exists
|
# Validate that termination A exists
|
||||||
if not hasattr(self, 'termination_a_type'):
|
if not hasattr(self, 'termination_a_type'):
|
||||||
raise ValidationError('Termination A type has not been specified')
|
raise ValidationError('Termination A type has not been specified')
|
||||||
|
@ -254,6 +254,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return yaml.dump(dict(data), sort_keys=False)
|
return yaml.dump(dict(data), sort_keys=False)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
|
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
|
||||||
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
||||||
@ -634,7 +635,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
super().validate_unique(exclude)
|
super().validate_unique(exclude)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate site/rack combination
|
# Validate site/rack combination
|
||||||
@ -917,6 +917,7 @@ class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||||
# VirtualChassis.)
|
# VirtualChassis.)
|
||||||
|
@ -64,6 +64,7 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# RackGroup must belong to assigned Site
|
# RackGroup must belong to assigned Site
|
||||||
if self.rack_group and self.rack_group.site != self.site:
|
if self.rack_group and self.rack_group.site != self.site:
|
||||||
@ -172,6 +173,7 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo
|
|||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# Rack must belong to same Site as PowerPanel
|
# Rack must belong to same Site as PowerPanel
|
||||||
if self.rack and self.rack.site != self.power_panel.site:
|
if self.rack and self.rack.site != self.power_panel.site:
|
||||||
|
@ -296,6 +296,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('dcim:rack', args=[self.pk])
|
return reverse('dcim:rack', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# Validate outer dimensions and unit
|
# Validate outer dimensions and unit
|
||||||
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
|
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
|
||||||
@ -602,6 +603,7 @@ class RackReservation(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('dcim:rackreservation', args=[self.pk])
|
return reverse('dcim:rackreservation', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
if hasattr(self, 'rack') and self.units:
|
if hasattr(self, 'rack') and self.units:
|
||||||
|
|
||||||
|
@ -43,6 +43,16 @@ class CustomFieldModel(models.Model):
|
|||||||
(field, self.custom_field_data.get(field.name)) for field in fields
|
(field, self.custom_field_data.get(field.name)) for field in fields
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate custom field data
|
||||||
|
custom_field_names = CustomField.objects.get_for_model(self).values_list('name', flat=True)
|
||||||
|
for field_name in self.custom_field_data:
|
||||||
|
if field_name not in custom_field_names:
|
||||||
|
raise ValidationError({
|
||||||
|
'custom_field_data': f'Unknown custom field: {field_name}'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldManager(models.Manager):
|
class CustomFieldManager(models.Manager):
|
||||||
use_in_migrations = True
|
use_in_migrations = True
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.forms import SiteCSVForm
|
from dcim.forms import SiteCSVForm
|
||||||
from dcim.models import Site
|
from dcim.models import Site, Rack
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
from utilities.testing import APITestCase, TestCase
|
from utilities.testing import APITestCase, TestCase
|
||||||
@ -534,3 +535,44 @@ class CustomFieldImportTest(TestCase):
|
|||||||
form = SiteCSVForm(data=form_data)
|
form = SiteCSVForm(data=form_data)
|
||||||
self.assertFalse(form.is_valid())
|
self.assertFalse(form.is_valid())
|
||||||
self.assertIn('cf_select', form.errors)
|
self.assertIn('cf_select', form.errors)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldModelTest(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
|
||||||
|
cf1.save()
|
||||||
|
cf1.content_types.set([ContentType.objects.get_for_model(Site)])
|
||||||
|
|
||||||
|
cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
|
||||||
|
cf2.save()
|
||||||
|
cf2.content_types.set([ContentType.objects.get_for_model(Rack)])
|
||||||
|
|
||||||
|
def test_cf_data(self):
|
||||||
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
|
||||||
|
# Check custom field data on new instance
|
||||||
|
site.cf['foo'] = 'abc'
|
||||||
|
self.assertEqual(site.cf['foo'], 'abc')
|
||||||
|
|
||||||
|
# Check custom field data from database
|
||||||
|
site.save()
|
||||||
|
site = Site.objects.get(name='Test Site')
|
||||||
|
self.assertEqual(site.cf['foo'], 'abc')
|
||||||
|
|
||||||
|
def test_invalid_data(self):
|
||||||
|
"""
|
||||||
|
Setting custom field data for a non-applicable (or non-existent) CustomField should raise a ValidationError.
|
||||||
|
"""
|
||||||
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
|
||||||
|
# Set custom field data
|
||||||
|
site.cf['foo'] = 'abc'
|
||||||
|
site.cf['bar'] = 'def'
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
site.clean()
|
||||||
|
|
||||||
|
del(site.cf['bar'])
|
||||||
|
site.clean()
|
||||||
|
@ -256,6 +256,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('ipam:aggregate', args=[self.pk])
|
return reverse('ipam:aggregate', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
|
|
||||||
@ -442,6 +443,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('ipam:prefix', args=[self.pk])
|
return reverse('ipam:prefix', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
|
|
||||||
@ -721,6 +723,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
).exclude(pk=self.pk)
|
).exclude(pk=self.pk)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
if self.address:
|
if self.address:
|
||||||
|
|
||||||
@ -970,6 +973,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('ipam:vlan', args=[self.pk])
|
return reverse('ipam:vlan', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# Validate VLAN group
|
# Validate VLAN group
|
||||||
if self.group and self.group.site != self.site:
|
if self.group and self.group.site != self.site:
|
||||||
@ -1078,6 +1082,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return self.device or self.virtual_machine
|
return self.device or self.virtual_machine
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# A Service must belong to a Device *or* to a VirtualMachine
|
# A Service must belong to a Device *or* to a VirtualMachine
|
||||||
if self.device and self.virtual_machine:
|
if self.device and self.virtual_machine:
|
||||||
|
@ -74,7 +74,8 @@ class UserKey(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user.username
|
return self.user.username
|
||||||
|
|
||||||
def clean(self, *args, **kwargs):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
if self.public_key:
|
if self.public_key:
|
||||||
|
|
||||||
@ -105,8 +106,6 @@ class UserKey(models.Model):
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Check whether public_key has been modified. If so, nullify the initial master_key_cipher.
|
# Check whether public_key has been modified. If so, nullify the initial master_key_cipher.
|
||||||
|
@ -172,6 +172,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return reverse('virtualization:cluster', args=[self.pk])
|
return reverse('virtualization:cluster', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
# If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
|
# If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
|
||||||
if self.pk and self.site:
|
if self.pk and self.site:
|
||||||
@ -317,7 +318,6 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
super().validate_unique(exclude)
|
super().validate_unique(exclude)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate primary IP addresses
|
# Validate primary IP addresses
|
||||||
|
Loading…
Reference in New Issue
Block a user