Jelajahi Sumber

Use case-insensitive collations on fields considered for uniqueness

Jeremy Stretch 3 bulan lalu
induk
melakukan
06052f8eaa

+ 3 - 2
netbox/circuits/models/circuits.py

@@ -41,9 +41,10 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
     ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
     ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
     """
     """
     cid = models.CharField(
     cid = models.CharField(
-        max_length=100,
         verbose_name=_('circuit ID'),
         verbose_name=_('circuit ID'),
-        help_text=_('Unique circuit ID')
+        max_length=100,
+        db_collation='case_insensitive',
+        help_text=_('Unique circuit ID'),
     )
     )
     provider = models.ForeignKey(
     provider = models.ForeignKey(
         to='circuits.Provider',
         to='circuits.Provider',

+ 8 - 5
netbox/circuits/models/providers.py

@@ -21,13 +21,14 @@ class Provider(ContactsMixin, PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
+        db_collation='ci_natural_sort',
         help_text=_('Full name of the provider'),
         help_text=_('Full name of the provider'),
-        db_collation="natural_sort"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
     asns = models.ManyToManyField(
     asns = models.ManyToManyField(
         to='ipam.ASN',
         to='ipam.ASN',
@@ -56,13 +57,15 @@ class ProviderAccount(ContactsMixin, PrimaryModel):
         related_name='accounts'
         related_name='accounts'
     )
     )
     account = models.CharField(
     account = models.CharField(
+        verbose_name=_('account ID'),
         max_length=100,
         max_length=100,
-        verbose_name=_('account ID')
+        db_collation='ci_natural_sort',
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        blank=True
+        db_collation='ci_natural_sort',
+        blank=True,
     )
     )
 
 
     clone_fields = ('provider', )
     clone_fields = ('provider', )
@@ -97,7 +100,7 @@ class ProviderNetwork(PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     provider = models.ForeignKey(
     provider = models.ForeignKey(
         to='circuits.Provider',
         to='circuits.Provider',

+ 3 - 2
netbox/circuits/models/virtual_circuits.py

@@ -34,9 +34,10 @@ class VirtualCircuit(PrimaryModel):
     A virtual connection between two or more endpoints, delivered across one or more physical circuits.
     A virtual connection between two or more endpoints, delivered across one or more physical circuits.
     """
     """
     cid = models.CharField(
     cid = models.CharField(
-        max_length=100,
         verbose_name=_('circuit ID'),
         verbose_name=_('circuit ID'),
-        help_text=_('Unique circuit ID')
+        max_length=100,
+        db_collation='case_insensitive',
+        help_text=_('Unique circuit ID'),
     )
     )
     provider_network = models.ForeignKey(
     provider_network = models.ForeignKey(
         to='circuits.ProviderNetwork',
         to='circuits.ProviderNetwork',

+ 2 - 1
netbox/core/models/data.py

@@ -38,7 +38,8 @@ class DataSource(JobsMixin, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     type = models.CharField(
     type = models.CharField(
         verbose_name=_('type'),
         verbose_name=_('type'),

+ 1 - 1
netbox/dcim/models/device_component_templates.py

@@ -43,10 +43,10 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
+        db_collation='ci_natural_sort',
         help_text=_(
         help_text=_(
             "{module} is accepted as a substitution for the module bay position when attached to a module type."
             "{module} is accepted as a substitution for the module bay position when attached to a module type."
         ),
         ),
-        db_collation="natural_sort"
     )
     )
     label = models.CharField(
     label = models.CharField(
         verbose_name=_('label'),
         verbose_name=_('label'),

+ 1 - 1
netbox/dcim/models/device_components.py

@@ -52,7 +52,7 @@ class ComponentModel(NetBoxModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     label = models.CharField(
     label = models.CharField(
         verbose_name=_('label'),
         verbose_name=_('label'),

+ 11 - 12
netbox/dcim/models/devices.py

@@ -1,8 +1,7 @@
 import decimal
 import decimal
-import yaml
-
 from functools import cached_property
 from functools import cached_property
 
 
+import yaml
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
@@ -10,7 +9,6 @@ from django.core.files.storage import default_storage
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.db.models import F, ProtectedError, prefetch_related_objects
 from django.db.models import F, ProtectedError, prefetch_related_objects
-from django.db.models.functions import Lower
 from django.db.models.signals import post_save
 from django.db.models.signals import post_save
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
@@ -25,8 +23,8 @@ from extras.querysets import ConfigContextModelQuerySet
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
 from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
-from netbox.models.mixins import WeightMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
+from netbox.models.mixins import WeightMixin
 from utilities.fields import ColorField, CounterCacheField
 from utilities.fields import ColorField, CounterCacheField
 from utilities.prefetch import get_prefetchable_fields
 from utilities.prefetch import get_prefetchable_fields
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin
@@ -34,7 +32,6 @@ from .device_components import *
 from .mixins import RenderConfigMixin
 from .mixins import RenderConfigMixin
 from .modules import Module
 from .modules import Module
 
 
-
 __all__ = (
 __all__ = (
     'Device',
     'Device',
     'DeviceRole',
     'DeviceRole',
@@ -83,11 +80,13 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
     )
     )
     model = models.CharField(
     model = models.CharField(
         verbose_name=_('model'),
         verbose_name=_('model'),
-        max_length=100
+        max_length=100,
+        db_collation='case_insensitive',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
-        max_length=100
+        max_length=100,
+        db_collation='case_insensitive',
     )
     )
     default_platform = models.ForeignKey(
     default_platform = models.ForeignKey(
         to='dcim.Platform',
         to='dcim.Platform',
@@ -525,7 +524,7 @@ class Device(
         max_length=64,
         max_length=64,
         blank=True,
         blank=True,
         null=True,
         null=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     serial = models.CharField(
     serial = models.CharField(
         max_length=50,
         max_length=50,
@@ -721,11 +720,11 @@ class Device(
         ordering = ('name', 'pk')  # Name may be null
         ordering = ('name', 'pk')  # Name may be null
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
-                Lower('name'), 'site', 'tenant',
+                'name', 'site', 'tenant',
                 name='%(app_label)s_%(class)s_unique_name_site_tenant'
                 name='%(app_label)s_%(class)s_unique_name_site_tenant'
             ),
             ),
             models.UniqueConstraint(
             models.UniqueConstraint(
-                Lower('name'), 'site',
+                'name', 'site',
                 name='%(app_label)s_%(class)s_unique_name_site',
                 name='%(app_label)s_%(class)s_unique_name_site',
                 condition=Q(tenant__isnull=True),
                 condition=Q(tenant__isnull=True),
                 violation_error_message=_("Device name must be unique per site.")
                 violation_error_message=_("Device name must be unique per site.")
@@ -1119,7 +1118,7 @@ class VirtualChassis(PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='natural_sort',
     )
     )
     domain = models.CharField(
     domain = models.CharField(
         verbose_name=_('domain'),
         verbose_name=_('domain'),
@@ -1182,7 +1181,7 @@ class VirtualDeviceContext(PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

+ 4 - 2
netbox/dcim/models/modules.py

@@ -31,7 +31,8 @@ class ModuleTypeProfile(PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     schema = models.JSONField(
     schema = models.JSONField(
         blank=True,
         blank=True,
@@ -72,7 +73,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
     )
     )
     model = models.CharField(
     model = models.CharField(
         verbose_name=_('model'),
         verbose_name=_('model'),
-        max_length=100
+        max_length=100,
+        db_collation='ci_natural_sort',
     )
     )
     part_number = models.CharField(
     part_number = models.CharField(
         verbose_name=_('part number'),
         verbose_name=_('part number'),

+ 2 - 2
netbox/dcim/models/power.py

@@ -37,7 +37,7 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
 
 
     prerequisite_models = (
     prerequisite_models = (
@@ -88,7 +88,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

+ 5 - 3
netbox/dcim/models/racks.py

@@ -137,12 +137,14 @@ class RackType(RackBase):
     )
     )
     model = models.CharField(
     model = models.CharField(
         verbose_name=_('model'),
         verbose_name=_('model'),
-        max_length=100
+        max_length=100,
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
 
 
     clone_fields = (
     clone_fields = (
@@ -262,7 +264,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     facility_id = models.CharField(
     facility_id = models.CharField(
         max_length=50,
         max_length=50,

+ 4 - 3
netbox/dcim/models/sites.py

@@ -142,13 +142,14 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        help_text=_("Full name of the site"),
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
+        help_text=_("Full name of the site")
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

+ 4 - 2
netbox/extras/models/configs.py

@@ -35,7 +35,8 @@ class ConfigContextProfile(SyncedDataMixin, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),
@@ -77,7 +78,8 @@ class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLogge
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     profile = models.ForeignKey(
     profile = models.ForeignKey(
         to='extras.ConfigContextProfile',
         to='extras.ConfigContextProfile',

+ 3 - 1
netbox/extras/models/customfields.py

@@ -94,6 +94,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=50,
         max_length=50,
         unique=True,
         unique=True,
+        db_collation='ci_natural_sort',
         help_text=_('Internal field name'),
         help_text=_('Internal field name'),
         validators=(
         validators=(
             RegexValidator(
             RegexValidator(
@@ -779,7 +780,8 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
     """
     """
     name = models.CharField(
     name = models.CharField(
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     description = models.CharField(
     description = models.CharField(
         max_length=200,
         max_length=200,

+ 10 - 5
netbox/extras/models/models.py

@@ -59,7 +59,8 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=150,
         max_length=150,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),
@@ -164,7 +165,8 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=150,
         max_length=150,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),
@@ -307,7 +309,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     enabled = models.BooleanField(
     enabled = models.BooleanField(
         verbose_name=_('enabled'),
         verbose_name=_('enabled'),
@@ -468,12 +471,14 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),

+ 2 - 1
netbox/extras/models/notifications.py

@@ -125,7 +125,8 @@ class NotificationGroup(ChangeLoggedModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),

+ 16 - 1
netbox/extras/models/tags.py

@@ -2,7 +2,7 @@ from django.conf import settings
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.text import slugify
 from django.utils.text import slugify
-from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext_lazy as _, pgettext_lazy
 from taggit.models import TagBase, GenericTaggedItemBase
 from taggit.models import TagBase, GenericTaggedItemBase
 
 
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
@@ -25,6 +25,21 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
     id = models.BigAutoField(
     id = models.BigAutoField(
         primary_key=True
         primary_key=True
     )
     )
+    # Override TagBase.name to set db_collation
+    name = models.CharField(
+        verbose_name=pgettext_lazy("A tag name", "name"),
+        unique=True,
+        max_length=100,
+        db_collation='ci_natural_sort',
+    )
+    # Override TagBase.slug to set db_collation
+    slug = models.SlugField(
+        verbose_name=pgettext_lazy("A tag slug", "slug"),
+        unique=True,
+        max_length=100,
+        allow_unicode=True,
+        db_collation='case_insensitive',
+    )
     color = ColorField(
     color = ColorField(
         verbose_name=_('color'),
         verbose_name=_('color'),
         default=ColorChoices.COLOR_GREY
         default=ColorChoices.COLOR_GREY

+ 1 - 6
netbox/ipam/models/asns.py

@@ -18,12 +18,7 @@ class ASNRange(OrganizationalModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
-    )
-    slug = models.SlugField(
-        verbose_name=_('slug'),
-        max_length=100,
-        unique=True
+        db_collation='ci_natural_sort',
     )
     )
     rir = models.ForeignKey(
     rir = models.ForeignKey(
         to='ipam.RIR',
         to='ipam.RIR',

+ 2 - 1
netbox/ipam/models/services.py

@@ -50,7 +50,8 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
 
 
     class Meta:
     class Meta:

+ 6 - 3
netbox/ipam/models/vlans.py

@@ -37,11 +37,12 @@ class VLANGroup(OrganizationalModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
-        max_length=100
+        max_length=100,
+        db_collation='case_insensitive',
     )
     )
     scope_type = models.ForeignKey(
     scope_type = models.ForeignKey(
         to='contenttypes.ContentType',
         to='contenttypes.ContentType',
@@ -214,7 +215,8 @@ class VLAN(PrimaryModel):
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
-        max_length=64
+        max_length=64,
+        db_collation='ci_natural_sort',
     )
     )
     tenant = models.ForeignKey(
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         to='tenancy.Tenant',
@@ -362,6 +364,7 @@ class VLANTranslationPolicy(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
+        db_collation='ci_natural_sort',
     )
     )
 
 
     class Meta:
     class Meta:

+ 3 - 2
netbox/ipam/models/vrfs.py

@@ -19,11 +19,12 @@ class VRF(PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='natural_sort',
     )
     )
     rd = models.CharField(
     rd = models.CharField(
         max_length=VRF_RD_MAX_LENGTH,
         max_length=VRF_RD_MAX_LENGTH,
         unique=True,
         unique=True,
+        db_collation='case_insensitive',
         blank=True,
         blank=True,
         null=True,
         null=True,
         verbose_name=_('route distinguisher'),
         verbose_name=_('route distinguisher'),
@@ -75,8 +76,8 @@ class RouteTarget(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=VRF_RD_MAX_LENGTH,  # Same format options as VRF RD (RFC 4360 section 4)
         max_length=VRF_RD_MAX_LENGTH,  # Same format options as VRF RD (RFC 4360 section 4)
         unique=True,
         unique=True,
+        db_collation='ci_natural_sort',
         help_text=_('Route target value (formatted in accordance with RFC 4360)'),
         help_text=_('Route target value (formatted in accordance with RFC 4360)'),
-        db_collation="natural_sort"
     )
     )
     tenant = models.ForeignKey(
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         to='tenancy.Tenant',

+ 8 - 4
netbox/netbox/models/__init__.py

@@ -153,11 +153,13 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
-        max_length=100
+        max_length=100,
+        db_collation='case_insensitive',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),
@@ -202,12 +204,14 @@ class OrganizationalModel(NetBoxModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),

+ 1 - 1
netbox/tenancy/models/contacts.py

@@ -55,7 +55,7 @@ class Contact(PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='natural_sort',
     )
     )
     title = models.CharField(
     title = models.CharField(
         verbose_name=_('title'),
         verbose_name=_('title'),

+ 6 - 4
netbox/tenancy/models/tenants.py

@@ -19,12 +19,13 @@ class TenantGroup(NestedGroupModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort'
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive'
     )
     )
 
 
     class Meta:
     class Meta:
@@ -41,11 +42,12 @@ class Tenant(ContactsMixin, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
-        max_length=100
+        max_length=100,
+        db_collation='case_insensitive',
     )
     )
     group = models.ForeignKey(
     group = models.ForeignKey(
         to='tenancy.TenantGroup',
         to='tenancy.TenantGroup',

+ 1 - 1
netbox/virtualization/models/clusters.py

@@ -51,7 +51,7 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     type = models.ForeignKey(
     type = models.ForeignKey(
         verbose_name=_('type'),
         verbose_name=_('type'),

+ 4 - 5
netbox/virtualization/models/virtualmachines.py

@@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator
 from django.core.validators import MinValueValidator
 from django.db import models
 from django.db import models
 from django.db.models import Q, Sum
 from django.db.models import Q, Sum
-from django.db.models.functions import Lower
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from dcim.models import BaseInterface
 from dcim.models import BaseInterface
@@ -70,7 +69,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     status = models.CharField(
     status = models.CharField(
         max_length=50,
         max_length=50,
@@ -156,11 +155,11 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
         ordering = ('name', 'pk')  # Name may be non-unique
         ordering = ('name', 'pk')  # Name may be non-unique
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
-                Lower('name'), 'cluster', 'tenant',
+                'name', 'cluster', 'tenant',
                 name='%(app_label)s_%(class)s_unique_name_cluster_tenant'
                 name='%(app_label)s_%(class)s_unique_name_cluster_tenant'
             ),
             ),
             models.UniqueConstraint(
             models.UniqueConstraint(
-                Lower('name'), 'cluster',
+                'name', 'cluster',
                 name='%(app_label)s_%(class)s_unique_name_cluster',
                 name='%(app_label)s_%(class)s_unique_name_cluster',
                 condition=Q(tenant__isnull=True),
                 condition=Q(tenant__isnull=True),
                 violation_error_message=_("Virtual machine name must be unique per cluster.")
                 violation_error_message=_("Virtual machine name must be unique per cluster.")
@@ -275,7 +274,7 @@ class ComponentModel(NetBoxModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),

+ 5 - 5
netbox/vpn/models/crypto.py

@@ -23,7 +23,7 @@ class IKEProposal(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     authentication_method = models.CharField(
     authentication_method = models.CharField(
         verbose_name=('authentication method'),
         verbose_name=('authentication method'),
@@ -69,7 +69,7 @@ class IKEPolicy(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     version = models.PositiveSmallIntegerField(
     version = models.PositiveSmallIntegerField(
         verbose_name=_('version'),
         verbose_name=_('version'),
@@ -128,7 +128,7 @@ class IPSecProposal(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     encryption_algorithm = models.CharField(
     encryption_algorithm = models.CharField(
         verbose_name=_('encryption'),
         verbose_name=_('encryption'),
@@ -180,7 +180,7 @@ class IPSecPolicy(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     proposals = models.ManyToManyField(
     proposals = models.ManyToManyField(
         to='vpn.IPSecProposal',
         to='vpn.IPSecProposal',
@@ -216,7 +216,7 @@ class IPSecProfile(PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     mode = models.CharField(
     mode = models.CharField(
         verbose_name=_('mode'),
         verbose_name=_('mode'),

+ 3 - 2
netbox/vpn/models/l2vpn.py

@@ -20,12 +20,13 @@ class L2VPN(ContactsMixin, PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
     type = models.CharField(
     type = models.CharField(
         verbose_name=_('type'),
         verbose_name=_('type'),

+ 1 - 1
netbox/vpn/models/tunnels.py

@@ -32,7 +32,7 @@ class Tunnel(ContactsMixin, PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

+ 3 - 2
netbox/wireless/models.py

@@ -53,12 +53,13 @@ class WirelessLANGroup(NestedGroupModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     )
 
 
     class Meta:
     class Meta: