Jeremy Stretch 6 лет назад
Родитель
Сommit
a8dd060e32
3 измененных файлов с 74 добавлено и 2 удалено
  1. 23 0
      netbox/dcim/migrations/0086_device_name_nonunique.py
  2. 14 2
      netbox/dcim/models.py
  3. 37 0
      netbox/dcim/tests/test_models.py

+ 23 - 0
netbox/dcim/migrations/0086_device_name_nonunique.py

@@ -0,0 +1,23 @@
+# Generated by Django 2.2.6 on 2019-12-09 15:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0006_custom_tag_models'),
+        ('dcim', '0085_3569_poweroutlet_fields'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='device',
+            name='name',
+            field=models.CharField(blank=True, max_length=64, null=True),
+        ),
+        migrations.AlterUniqueTogether(
+            name='device',
+            unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position'), ('site', 'tenant', 'name')},
+        ),
+    ]

+ 14 - 2
netbox/dcim/models.py

@@ -1523,8 +1523,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     name = models.CharField(
     name = models.CharField(
         max_length=64,
         max_length=64,
         blank=True,
         blank=True,
-        null=True,
-        unique=True
+        null=True
     )
     )
     serial = models.CharField(
     serial = models.CharField(
         max_length=50,
         max_length=50,
@@ -1645,6 +1644,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
         unique_together = [
         unique_together = [
+            ['site', 'tenant', 'name'],  # See validate_unique below
             ['rack', 'position', 'face'],
             ['rack', 'position', 'face'],
             ['virtual_chassis', 'vc_position'],
             ['virtual_chassis', 'vc_position'],
         ]
         ]
@@ -1659,6 +1659,18 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:device', args=[self.pk])
         return reverse('dcim:device', args=[self.pk])
 
 
+    def validate_unique(self, exclude=None):
+
+        # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
+        # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
+        # of the uniqueness constraint without manual intervention.
+        if self.tenant is None and Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
+            raise ValidationError({
+                'name': 'A device with this name already exists.'
+            })
+
+        super().validate_unique(exclude)
+
     def clean(self):
     def clean(self):
 
 
         super().clean()
         super().clean()

+ 37 - 0
netbox/dcim/tests/test_models.py

@@ -1,6 +1,7 @@
 from django.test import TestCase
 from django.test import TestCase
 
 
 from dcim.models import *
 from dcim.models import *
+from tenancy.models import Tenant
 
 
 
 
 class RackTestCase(TestCase):
 class RackTestCase(TestCase):
@@ -281,6 +282,42 @@ class DeviceTestCase(TestCase):
             name='Device Bay 1'
             name='Device Bay 1'
         )
         )
 
 
+    def test_device_duplicate_name_per_site(self):
+
+        device1 = Device(
+            site=self.site,
+            device_type=self.device_type,
+            device_role=self.device_role,
+            name='Test Device 1'
+        )
+        device1.save()
+
+        device2 = Device(
+            site=device1.site,
+            device_type=device1.device_type,
+            device_role=device1.device_role,
+            name=device1.name
+        )
+
+        # Two devices assigned to the same Site and no Tenant should fail validation
+        with self.assertRaises(ValidationError):
+            device2.full_clean()
+
+        tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
+        device1.tenant = tenant
+        device1.save()
+        device2.tenant = tenant
+
+        # Two devices assigned to the same Site and the same Tenant should fail validation
+        with self.assertRaises(ValidationError):
+            device2.full_clean()
+
+        device2.tenant = None
+
+        # Two devices assigned to the same Site and different Tenants should pass validation
+        device2.full_clean()
+        device2.save()
+
 
 
 class CableTestCase(TestCase):
 class CableTestCase(TestCase):