diff --git a/netbox/dcim/migrations/0241_consolidate_unique_constraints.py b/netbox/dcim/migrations/0241_consolidate_unique_constraints.py new file mode 100644 index 000000000..9e54632d3 --- /dev/null +++ b/netbox/dcim/migrations/0241_consolidate_unique_constraints.py @@ -0,0 +1,204 @@ +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0240_device__config_context_data'), + ('extras', '0139_alter_customfieldchoiceset_extra_choices'), + ('tenancy', '0025_ltree_paths'), + ('users', '0016_default_ordering_indexes'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='devicerole', + name='dcim_devicerole_parent_name', + ), + migrations.RemoveConstraint( + model_name='devicerole', + name='dcim_devicerole_name', + ), + migrations.RemoveConstraint( + model_name='devicerole', + name='dcim_devicerole_parent_slug', + ), + migrations.RemoveConstraint( + model_name='devicerole', + name='dcim_devicerole_slug', + ), + migrations.RemoveConstraint( + model_name='location', + name='dcim_location_parent_name', + ), + migrations.RemoveConstraint( + model_name='location', + name='dcim_location_parent_slug', + ), + migrations.RemoveConstraint( + model_name='location', + name='dcim_location_name', + ), + migrations.RemoveConstraint( + model_name='location', + name='dcim_location_slug', + ), + migrations.RemoveConstraint( + model_name='platform', + name='dcim_platform_manufacturer_name', + ), + migrations.RemoveConstraint( + model_name='platform', + name='dcim_platform_name', + ), + migrations.RemoveConstraint( + model_name='platform', + name='dcim_platform_manufacturer_slug', + ), + migrations.RemoveConstraint( + model_name='platform', + name='dcim_platform_slug', + ), + migrations.RemoveConstraint( + model_name='region', + name='dcim_region_parent_name', + ), + migrations.RemoveConstraint( + model_name='region', + name='dcim_region_parent_slug', + ), + migrations.RemoveConstraint( + model_name='region', + name='dcim_region_name', + ), + migrations.RemoveConstraint( + model_name='region', + name='dcim_region_slug', + ), + migrations.RemoveConstraint( + model_name='sitegroup', + name='dcim_sitegroup_parent_name', + ), + migrations.RemoveConstraint( + model_name='sitegroup', + name='dcim_sitegroup_parent_slug', + ), + migrations.RemoveConstraint( + model_name='sitegroup', + name='dcim_sitegroup_name', + ), + migrations.RemoveConstraint( + model_name='sitegroup', + name='dcim_sitegroup_slug', + ), + migrations.RemoveConstraint( + model_name='device', + name='dcim_device_unique_name_site_tenant', + ), + migrations.RemoveConstraint( + model_name='device', + name='dcim_device_unique_name_site', + ), + migrations.AddConstraint( + model_name='devicerole', + constraint=models.UniqueConstraint( + fields=('parent', 'name'), + name='dcim_devicerole_parent_name', + nulls_distinct=False, + violation_error_message='A device role with this name already exists.', + ), + ), + migrations.AddConstraint( + model_name='devicerole', + constraint=models.UniqueConstraint( + fields=('parent', 'slug'), + name='dcim_devicerole_parent_slug', + nulls_distinct=False, + violation_error_message='A device role with this slug already exists.', + ), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint( + fields=('site', 'parent', 'name'), + name='dcim_location_parent_name', + nulls_distinct=False, + violation_error_message='A location with this name already exists within the specified site.', + ), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint( + fields=('site', 'parent', 'slug'), + name='dcim_location_parent_slug', + nulls_distinct=False, + violation_error_message='A location with this slug already exists within the specified site.', + ), + ), + migrations.AddConstraint( + model_name='platform', + constraint=models.UniqueConstraint( + fields=('manufacturer', 'name'), + name='dcim_platform_manufacturer_name', + nulls_distinct=False, + violation_error_message='Platform name must be unique.', + ), + ), + migrations.AddConstraint( + model_name='platform', + constraint=models.UniqueConstraint( + fields=('manufacturer', 'slug'), + name='dcim_platform_manufacturer_slug', + nulls_distinct=False, + violation_error_message='Platform slug must be unique.', + ), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint( + fields=('parent', 'name'), + name='dcim_region_parent_name', + nulls_distinct=False, + violation_error_message='A region with this name already exists.', + ), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint( + fields=('parent', 'slug'), + name='dcim_region_parent_slug', + nulls_distinct=False, + violation_error_message='A region with this slug already exists.', + ), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint( + fields=('parent', 'name'), + name='dcim_sitegroup_parent_name', + nulls_distinct=False, + violation_error_message='A site group with this name already exists.', + ), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint( + fields=('parent', 'slug'), + name='dcim_sitegroup_parent_slug', + nulls_distinct=False, + violation_error_message='A site group with this slug already exists.', + ), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower('name'), + models.F('site'), + models.F('tenant'), + condition=models.Q(('name__isnull', False)), + name='dcim_device_unique_name_site_tenant', + nulls_distinct=False, + violation_error_message='Device name must be unique per site and tenant.', + ), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 8da87f8f8..702b091a3 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -420,23 +420,15 @@ class DeviceRole(NestedLtreeGroupModel): constraints = ( models.UniqueConstraint( fields=('parent', 'name'), - name='%(app_label)s_%(class)s_parent_name' - ), - models.UniqueConstraint( - fields=('name',), - name='%(app_label)s_%(class)s_name', - condition=Q(parent__isnull=True), - violation_error_message=_("A top-level device role with this name already exists.") + name='%(app_label)s_%(class)s_parent_name', + nulls_distinct=False, + violation_error_message=_("A device role with this name already exists.") ), models.UniqueConstraint( fields=('parent', 'slug'), - name='%(app_label)s_%(class)s_parent_slug' - ), - models.UniqueConstraint( - fields=('slug',), - name='%(app_label)s_%(class)s_slug', - condition=Q(parent__isnull=True), - violation_error_message=_("A top-level device role with this slug already exists.") + name='%(app_label)s_%(class)s_parent_slug', + nulls_distinct=False, + violation_error_message=_("A device role with this slug already exists.") ), ) verbose_name = _('device role') @@ -478,21 +470,13 @@ class Platform(NestedLtreeGroupModel): models.UniqueConstraint( fields=('manufacturer', 'name'), name='%(app_label)s_%(class)s_manufacturer_name', - ), - models.UniqueConstraint( - fields=('name',), - name='%(app_label)s_%(class)s_name', - condition=Q(manufacturer__isnull=True), + nulls_distinct=False, violation_error_message=_("Platform name must be unique.") ), models.UniqueConstraint( fields=('manufacturer', 'slug'), name='%(app_label)s_%(class)s_manufacturer_slug', - ), - models.UniqueConstraint( - fields=('slug',), - name='%(app_label)s_%(class)s_slug', - condition=Q(manufacturer__isnull=True), + nulls_distinct=False, violation_error_message=_("Platform slug must be unique.") ), ) @@ -764,13 +748,10 @@ class Device( constraints = ( models.UniqueConstraint( Lower('name'), 'site', 'tenant', - name='%(app_label)s_%(class)s_unique_name_site_tenant' - ), - models.UniqueConstraint( - Lower('name'), 'site', - name='%(app_label)s_%(class)s_unique_name_site', - condition=Q(tenant__isnull=True), - violation_error_message=_("Device name must be unique per site.") + name='%(app_label)s_%(class)s_unique_name_site_tenant', + condition=Q(name__isnull=False), + nulls_distinct=False, + violation_error_message=_("Device name must be unique per site and tenant.") ), models.UniqueConstraint( fields=('rack', 'position', 'face'), diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 16877d6da..441dbec1a 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -53,23 +53,15 @@ class Region(ContactsMixin, NestedLtreeGroupModel): constraints = ( models.UniqueConstraint( fields=('parent', 'name'), - name='%(app_label)s_%(class)s_parent_name' - ), - models.UniqueConstraint( - fields=('name',), - name='%(app_label)s_%(class)s_name', - condition=Q(parent__isnull=True), - violation_error_message=_("A top-level region with this name already exists.") + name='%(app_label)s_%(class)s_parent_name', + nulls_distinct=False, + violation_error_message=_("A region with this name already exists.") ), models.UniqueConstraint( fields=('parent', 'slug'), - name='%(app_label)s_%(class)s_parent_slug' - ), - models.UniqueConstraint( - fields=('slug',), - name='%(app_label)s_%(class)s_slug', - condition=Q(parent__isnull=True), - violation_error_message=_("A top-level region with this slug already exists.") + name='%(app_label)s_%(class)s_parent_slug', + nulls_distinct=False, + violation_error_message=_("A region with this slug already exists.") ), ) verbose_name = _('region') @@ -114,23 +106,15 @@ class SiteGroup(ContactsMixin, NestedLtreeGroupModel): constraints = ( models.UniqueConstraint( fields=('parent', 'name'), - name='%(app_label)s_%(class)s_parent_name' - ), - models.UniqueConstraint( - fields=('name',), - name='%(app_label)s_%(class)s_name', - condition=Q(parent__isnull=True), - violation_error_message=_("A top-level site group with this name already exists.") + name='%(app_label)s_%(class)s_parent_name', + nulls_distinct=False, + violation_error_message=_("A site group with this name already exists.") ), models.UniqueConstraint( fields=('parent', 'slug'), - name='%(app_label)s_%(class)s_parent_slug' - ), - models.UniqueConstraint( - fields=('slug',), - name='%(app_label)s_%(class)s_slug', - condition=Q(parent__isnull=True), - violation_error_message=_("A top-level site group with this slug already exists.") + name='%(app_label)s_%(class)s_parent_slug', + nulls_distinct=False, + violation_error_message=_("A site group with this slug already exists.") ), ) verbose_name = _('site group') @@ -341,22 +325,14 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedLtreeGroupModel): constraints = ( models.UniqueConstraint( fields=('site', 'parent', 'name'), - name='%(app_label)s_%(class)s_parent_name' - ), - models.UniqueConstraint( - fields=('site', 'name'), - name='%(app_label)s_%(class)s_name', - condition=Q(parent__isnull=True), + name='%(app_label)s_%(class)s_parent_name', + nulls_distinct=False, violation_error_message=_("A location with this name already exists within the specified site.") ), models.UniqueConstraint( fields=('site', 'parent', 'slug'), - name='%(app_label)s_%(class)s_parent_slug' - ), - models.UniqueConstraint( - fields=('site', 'slug'), - name='%(app_label)s_%(class)s_slug', - condition=Q(parent__isnull=True), + name='%(app_label)s_%(class)s_parent_slug', + nulls_distinct=False, violation_error_message=_("A location with this slug already exists within the specified site.") ), ) diff --git a/netbox/tenancy/migrations/0026_consolidate_unique_constraints.py b/netbox/tenancy/migrations/0026_consolidate_unique_constraints.py new file mode 100644 index 000000000..6790f05d2 --- /dev/null +++ b/netbox/tenancy/migrations/0026_consolidate_unique_constraints.py @@ -0,0 +1,46 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0139_alter_customfieldchoiceset_extra_choices'), + ('tenancy', '0025_ltree_paths'), + ('users', '0016_default_ordering_indexes'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='tenant', + name='tenancy_tenant_unique_group_name', + ), + migrations.RemoveConstraint( + model_name='tenant', + name='tenancy_tenant_unique_name', + ), + migrations.RemoveConstraint( + model_name='tenant', + name='tenancy_tenant_unique_group_slug', + ), + migrations.RemoveConstraint( + model_name='tenant', + name='tenancy_tenant_unique_slug', + ), + migrations.AddConstraint( + model_name='tenant', + constraint=models.UniqueConstraint( + fields=('group', 'name'), + name='tenancy_tenant_unique_group_name', + nulls_distinct=False, + violation_error_message='Tenant name must be unique per group.', + ), + ), + migrations.AddConstraint( + model_name='tenant', + constraint=models.UniqueConstraint( + fields=('group', 'slug'), + name='tenancy_tenant_unique_group_slug', + nulls_distinct=False, + violation_error_message='Tenant slug must be unique per group.', + ), + ), + ] diff --git a/netbox/tenancy/models/tenants.py b/netbox/tenancy/models/tenants.py index 95cdfafb8..f07aff797 100644 --- a/netbox/tenancy/models/tenants.py +++ b/netbox/tenancy/models/tenants.py @@ -1,6 +1,5 @@ from django.contrib.postgres.indexes import GistIndex from django.db import models -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from netbox.models import NestedLtreeGroupModel, PrimaryModel @@ -71,23 +70,15 @@ class Tenant(ContactsMixin, PrimaryModel): models.UniqueConstraint( fields=('group', 'name'), name='%(app_label)s_%(class)s_unique_group_name', + nulls_distinct=False, violation_error_message=_("Tenant name must be unique per group.") ), - models.UniqueConstraint( - fields=('name',), - name='%(app_label)s_%(class)s_unique_name', - condition=Q(group__isnull=True) - ), models.UniqueConstraint( fields=('group', 'slug'), name='%(app_label)s_%(class)s_unique_group_slug', + nulls_distinct=False, violation_error_message=_("Tenant slug must be unique per group.") ), - models.UniqueConstraint( - fields=('slug',), - name='%(app_label)s_%(class)s_unique_slug', - condition=Q(group__isnull=True) - ), ) verbose_name = _('tenant') verbose_name_plural = _('tenants') diff --git a/netbox/virtualization/migrations/0059_consolidate_unique_constraints.py b/netbox/virtualization/migrations/0059_consolidate_unique_constraints.py new file mode 100644 index 000000000..b56f0bdb2 --- /dev/null +++ b/netbox/virtualization/migrations/0059_consolidate_unique_constraints.py @@ -0,0 +1,56 @@ +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0241_consolidate_unique_constraints'), + ('extras', '0139_alter_customfieldchoiceset_extra_choices'), + ('ipam', '0093_denormalization_triggers'), + ('tenancy', '0026_consolidate_unique_constraints'), + ('users', '0016_default_ordering_indexes'), + ('virtualization', '0058_virtualmachine__config_context_data'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='virtualmachine', + name='virtualization_virtualmachine_unique_name_cluster_tenant', + ), + migrations.RemoveConstraint( + model_name='virtualmachine', + name='virtualization_virtualmachine_unique_name_cluster', + ), + migrations.RemoveConstraint( + model_name='virtualmachine', + name='virtualization_virtualmachine_unique_name_device_tenant', + ), + migrations.RemoveConstraint( + model_name='virtualmachine', + name='virtualization_virtualmachine_unique_name_device', + ), + migrations.AddConstraint( + model_name='virtualmachine', + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower('name'), + models.F('cluster'), + models.F('tenant'), + condition=models.Q(('cluster__isnull', False)), + name='virtualization_virtualmachine_unique_name_cluster_tenant', + nulls_distinct=False, + violation_error_message='Virtual machine name must be unique per cluster and tenant.', + ), + ), + migrations.AddConstraint( + model_name='virtualmachine', + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower('name'), + models.F('device'), + models.F('tenant'), + condition=models.Q(('cluster__isnull', True), ('device__isnull', False)), + name='virtualization_virtualmachine_unique_name_device_tenant', + nulls_distinct=False, + violation_error_message='Virtual machine name must be unique per device and tenant.', + ), + ), + ] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 6e72e3dd1..4763031a7 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -264,26 +264,17 @@ class VirtualMachine( models.UniqueConstraint( Lower('name'), 'cluster', 'tenant', name='%(app_label)s_%(class)s_unique_name_cluster_tenant', + condition=Q(cluster__isnull=False), + nulls_distinct=False, violation_error_message=_('Virtual machine name must be unique per cluster and tenant.') ), - models.UniqueConstraint( - Lower('name'), 'cluster', - name='%(app_label)s_%(class)s_unique_name_cluster', - condition=Q(tenant__isnull=True), - violation_error_message=_('Virtual machine name must be unique per cluster.') - ), models.UniqueConstraint( Lower('name'), 'device', 'tenant', name='%(app_label)s_%(class)s_unique_name_device_tenant', condition=Q(cluster__isnull=True, device__isnull=False), + nulls_distinct=False, violation_error_message=_('Virtual machine name must be unique per device and tenant.') ), - models.UniqueConstraint( - Lower('name'), 'device', - name='%(app_label)s_%(class)s_unique_name_device', - condition=Q(cluster__isnull=True, device__isnull=False, tenant__isnull=True), - violation_error_message=_('Virtual machine name must be unique per device.') - ), ) verbose_name = _('virtual machine') verbose_name_plural = _('virtual machines') diff --git a/netbox/vpn/migrations/0013_consolidate_unique_constraints.py b/netbox/vpn/migrations/0013_consolidate_unique_constraints.py new file mode 100644 index 000000000..cf49fa5ec --- /dev/null +++ b/netbox/vpn/migrations/0013_consolidate_unique_constraints.py @@ -0,0 +1,21 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0139_alter_customfieldchoiceset_extra_choices'), + ('tenancy', '0026_consolidate_unique_constraints'), + ('users', '0016_default_ordering_indexes'), + ('vpn', '0012_default_ordering_indexes'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='tunnel', + name='vpn_tunnel_group_name', + ), + migrations.RemoveConstraint( + model_name='tunnel', + name='vpn_tunnel_name', + ), + ] diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py index 76ea76327..4c1736b29 100644 --- a/netbox/vpn/models/tunnels.py +++ b/netbox/vpn/models/tunnels.py @@ -1,7 +1,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import ValidationError from django.db import models -from django.db.models import Q from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -78,17 +77,6 @@ class Tunnel(ContactsMixin, PrimaryModel): class Meta: ordering = ('name',) - constraints = ( - models.UniqueConstraint( - fields=('group', 'name'), - name='%(app_label)s_%(class)s_group_name' - ), - models.UniqueConstraint( - fields=('name',), - name='%(app_label)s_%(class)s_name', - condition=Q(group__isnull=True) - ), - ) verbose_name = _('tunnel') verbose_name_plural = _('tunnels') diff --git a/netbox/wireless/migrations/0022_wirelesslangroup_drop_unique_constraint.py b/netbox/wireless/migrations/0022_wirelesslangroup_drop_unique_constraint.py new file mode 100644 index 000000000..0115d5a4d --- /dev/null +++ b/netbox/wireless/migrations/0022_wirelesslangroup_drop_unique_constraint.py @@ -0,0 +1,16 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0139_alter_customfieldchoiceset_extra_choices'), + ('users', '0016_default_ordering_indexes'), + ('wireless', '0021_denormalization_triggers'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='wirelesslangroup', + name='wireless_wirelesslangroup_unique_parent_name', + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index d3618d513..42034afe2 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -70,12 +70,6 @@ class WirelessLANGroup(NestedLtreeGroupModel): GistIndex(fields=['path'], name='wireless_lan_grp_path_gist'), models.Index(fields=['sort_path'], name='wireless_lan_grp_sort_idx'), ) - constraints = ( - models.UniqueConstraint( - fields=('parent', 'name'), - name='%(app_label)s_%(class)s_unique_parent_name' - ), - ) verbose_name = _('wireless LAN group') verbose_name_plural = _('wireless LAN groups')