Skip to content

Commit b43980d

Browse files
committed
Fixes #7960: Prevent creation of regions/site groups/locations with duplicate names (see #7354)
1 parent 09b6125 commit b43980d

File tree

3 files changed

+148
-19
lines changed

3 files changed

+148
-19
lines changed

docs/release-notes/version-3.1.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* [#7771](https://github.com/netbox-community/netbox/issues/7771) - Group assignment should be optional when creating contacts via REST API
2828
* [#7849](https://github.com/netbox-community/netbox/issues/7849) - Fix exception when creating an FHRPGroup with an invalid IP address
2929
* [#7880](https://github.com/netbox-community/netbox/issues/7880) - Include assigned IP addresses in FHRP group object representation
30+
* [#7960](https://github.com/netbox-community/netbox/issues/7960) - Prevent creation of regions/site groups/locations with duplicate names (see #7354)
3031

3132
### REST API Changes
3233

netbox/dcim/migrations/0137_relax_uniqueness_constraints.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# Generated by Django 3.2.8 on 2021-10-19 17:41
2-
31
from django.db import migrations, models
42

53

@@ -32,14 +30,54 @@ class Migration(migrations.Migration):
3230
),
3331
migrations.AlterUniqueTogether(
3432
name='location',
35-
unique_together={('site', 'parent', 'name'), ('site', 'parent', 'slug')},
33+
unique_together=set(),
3634
),
37-
migrations.AlterUniqueTogether(
38-
name='region',
39-
unique_together={('parent', 'slug'), ('parent', 'name')},
35+
migrations.AddConstraint(
36+
model_name='location',
37+
constraint=models.UniqueConstraint(fields=('site', 'parent', 'name'), name='dcim_location_parent_name'),
4038
),
41-
migrations.AlterUniqueTogether(
42-
name='sitegroup',
43-
unique_together={('parent', 'slug'), ('parent', 'name')},
39+
migrations.AddConstraint(
40+
model_name='location',
41+
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'name'), name='dcim_location_name'),
42+
),
43+
migrations.AddConstraint(
44+
model_name='location',
45+
constraint=models.UniqueConstraint(fields=('site', 'parent', 'slug'), name='dcim_location_parent_slug'),
46+
),
47+
migrations.AddConstraint(
48+
model_name='location',
49+
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('site', 'slug'), name='dcim_location_slug'),
50+
),
51+
migrations.AddConstraint(
52+
model_name='region',
53+
constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_region_parent_name'),
54+
),
55+
migrations.AddConstraint(
56+
model_name='region',
57+
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_region_name'),
58+
),
59+
migrations.AddConstraint(
60+
model_name='region',
61+
constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_region_parent_slug'),
62+
),
63+
migrations.AddConstraint(
64+
model_name='region',
65+
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_region_slug'),
66+
),
67+
migrations.AddConstraint(
68+
model_name='sitegroup',
69+
constraint=models.UniqueConstraint(fields=('parent', 'name'), name='dcim_sitegroup_parent_name'),
70+
),
71+
migrations.AddConstraint(
72+
model_name='sitegroup',
73+
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('name',), name='dcim_sitegroup_name'),
74+
),
75+
migrations.AddConstraint(
76+
model_name='sitegroup',
77+
constraint=models.UniqueConstraint(fields=('parent', 'slug'), name='dcim_sitegroup_parent_slug'),
78+
),
79+
migrations.AddConstraint(
80+
model_name='sitegroup',
81+
constraint=models.UniqueConstraint(condition=models.Q(('parent', None)), fields=('slug',), name='dcim_sitegroup_slug'),
4482
),
4583
]

netbox/dcim/models/sites.py

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,41 @@ class Region(NestedGroupModel):
6262
)
6363

6464
class Meta:
65-
unique_together = (
66-
('parent', 'name'),
67-
('parent', 'slug'),
65+
constraints = (
66+
models.UniqueConstraint(
67+
fields=('parent', 'name'),
68+
name='dcim_region_parent_name'
69+
),
70+
models.UniqueConstraint(
71+
fields=('name',),
72+
name='dcim_region_name',
73+
condition=Q(parent=None)
74+
),
75+
models.UniqueConstraint(
76+
fields=('parent', 'slug'),
77+
name='dcim_region_parent_slug'
78+
),
79+
models.UniqueConstraint(
80+
fields=('slug',),
81+
name='dcim_region_slug',
82+
condition=Q(parent=None)
83+
),
6884
)
6985

86+
def validate_unique(self, exclude=None):
87+
if self.parent is None:
88+
regions = Region.objects.exclude(pk=self.pk)
89+
if regions.filter(name=self.name, parent__isnull=True).exists():
90+
raise ValidationError({
91+
'name': 'A region with this name already exists.'
92+
})
93+
if regions.filter(slug=self.slug, parent__isnull=True).exists():
94+
raise ValidationError({
95+
'name': 'A region with this slug already exists.'
96+
})
97+
98+
super().validate_unique(exclude=exclude)
99+
70100
def get_absolute_url(self):
71101
return reverse('dcim:region', args=[self.pk])
72102

@@ -119,11 +149,41 @@ class SiteGroup(NestedGroupModel):
119149
)
120150

121151
class Meta:
122-
unique_together = (
123-
('parent', 'name'),
124-
('parent', 'slug'),
152+
constraints = (
153+
models.UniqueConstraint(
154+
fields=('parent', 'name'),
155+
name='dcim_sitegroup_parent_name'
156+
),
157+
models.UniqueConstraint(
158+
fields=('name',),
159+
name='dcim_sitegroup_name',
160+
condition=Q(parent=None)
161+
),
162+
models.UniqueConstraint(
163+
fields=('parent', 'slug'),
164+
name='dcim_sitegroup_parent_slug'
165+
),
166+
models.UniqueConstraint(
167+
fields=('slug',),
168+
name='dcim_sitegroup_slug',
169+
condition=Q(parent=None)
170+
),
125171
)
126172

173+
def validate_unique(self, exclude=None):
174+
if self.parent is None:
175+
site_groups = SiteGroup.objects.exclude(pk=self.pk)
176+
if site_groups.filter(name=self.name, parent__isnull=True).exists():
177+
raise ValidationError({
178+
'name': 'A site group with this name already exists.'
179+
})
180+
if site_groups.filter(slug=self.slug, parent__isnull=True).exists():
181+
raise ValidationError({
182+
'name': 'A site group with this slug already exists.'
183+
})
184+
185+
super().validate_unique(exclude=exclude)
186+
127187
def get_absolute_url(self):
128188
return reverse('dcim:sitegroup', args=[self.pk])
129189

@@ -335,10 +395,40 @@ class Location(NestedGroupModel):
335395

336396
class Meta:
337397
ordering = ['site', 'name']
338-
unique_together = ([
339-
('site', 'parent', 'name'),
340-
('site', 'parent', 'slug'),
341-
])
398+
constraints = (
399+
models.UniqueConstraint(
400+
fields=('site', 'parent', 'name'),
401+
name='dcim_location_parent_name'
402+
),
403+
models.UniqueConstraint(
404+
fields=('site', 'name'),
405+
name='dcim_location_name',
406+
condition=Q(parent=None)
407+
),
408+
models.UniqueConstraint(
409+
fields=('site', 'parent', 'slug'),
410+
name='dcim_location_parent_slug'
411+
),
412+
models.UniqueConstraint(
413+
fields=('site', 'slug'),
414+
name='dcim_location_slug',
415+
condition=Q(parent=None)
416+
),
417+
)
418+
419+
def validate_unique(self, exclude=None):
420+
if self.parent is None:
421+
locations = Location.objects.exclude(pk=self.pk)
422+
if locations.filter(name=self.name, site=self.site, parent__isnull=True).exists():
423+
raise ValidationError({
424+
"name": f"A location with this name in site {self.site} already exists."
425+
})
426+
if locations.filter(slug=self.slug, site=self.site, parent__isnull=True).exists():
427+
raise ValidationError({
428+
"name": f"A location with this slug in site {self.site} already exists."
429+
})
430+
431+
super().validate_unique(exclude=exclude)
342432

343433
def get_absolute_url(self):
344434
return reverse('dcim:location', args=[self.pk])

0 commit comments

Comments
 (0)