mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Check for extraneous custom field data on clean()
This commit is contained in:
@ -155,6 +155,8 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
||||
def clean(self):
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
super().clean()
|
||||
|
||||
# Validate that termination A exists
|
||||
if not hasattr(self, 'termination_a_type'):
|
||||
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)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# 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
|
||||
@ -634,7 +635,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
super().validate_unique(exclude)
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
# Validate site/rack combination
|
||||
@ -917,6 +917,7 @@ class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
|
||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||
# VirtualChassis.)
|
||||
|
@ -64,6 +64,7 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# RackGroup must belong to assigned 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):
|
||||
super().clean()
|
||||
|
||||
# Rack must belong to same Site as PowerPanel
|
||||
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])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate outer dimensions and 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])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
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
|
||||
])
|
||||
|
||||
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):
|
||||
use_in_migrations = True
|
||||
|
@ -1,9 +1,10 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.forms import SiteCSVForm
|
||||
from dcim.models import Site
|
||||
from dcim.models import Site, Rack
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField
|
||||
from utilities.testing import APITestCase, TestCase
|
||||
@ -534,3 +535,44 @@ class CustomFieldImportTest(TestCase):
|
||||
form = SiteCSVForm(data=form_data)
|
||||
self.assertFalse(form.is_valid())
|
||||
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])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.prefix:
|
||||
|
||||
@ -442,6 +443,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
return reverse('ipam:prefix', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.prefix:
|
||||
|
||||
@ -721,6 +723,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
).exclude(pk=self.pk)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.address:
|
||||
|
||||
@ -970,6 +973,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
||||
return reverse('ipam:vlan', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate VLAN group
|
||||
if self.group and self.group.site != self.site:
|
||||
@ -1078,6 +1082,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
return self.device or self.virtual_machine
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# A Service must belong to a Device *or* to a VirtualMachine
|
||||
if self.device and self.virtual_machine:
|
||||
|
@ -74,7 +74,8 @@ class UserKey(models.Model):
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.public_key:
|
||||
|
||||
@ -105,8 +106,6 @@ class UserKey(models.Model):
|
||||
)
|
||||
})
|
||||
|
||||
super().clean()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# 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])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
|
||||
if self.pk and self.site:
|
||||
@ -317,7 +318,6 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
super().validate_unique(exclude)
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
# Validate primary IP addresses
|
||||
|
Reference in New Issue
Block a user