Просмотр исходного кода

Use case-insensitive collations on fields considered for uniqueness

Jeremy Stretch 3 месяцев назад
Родитель
Сommit
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.
     """
     cid = models.CharField(
-        max_length=100,
         verbose_name=_('circuit ID'),
-        help_text=_('Unique circuit ID')
+        max_length=100,
+        db_collation='case_insensitive',
+        help_text=_('Unique circuit ID'),
     )
     provider = models.ForeignKey(
         to='circuits.Provider',

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -142,13 +142,14 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
         verbose_name=_('name'),
         max_length=100,
         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(
         verbose_name=_('slug'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='case_insensitive',
     )
     status = models.CharField(
         verbose_name=_('status'),

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

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

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

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

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

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

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

@@ -125,7 +125,8 @@ class NotificationGroup(ChangeLoggedModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation='ci_natural_sort',
     )
     description = models.CharField(
         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.urls import reverse
 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 netbox.choices import ColorChoices
@@ -25,6 +25,21 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
     id = models.BigAutoField(
         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(
         verbose_name=_('color'),
         default=ColorChoices.COLOR_GREY

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

@@ -18,12 +18,7 @@ class ASNRange(OrganizationalModel):
         verbose_name=_('name'),
         max_length=100,
         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(
         to='ipam.RIR',

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -51,7 +51,7 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     type = models.ForeignKey(
         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.db import models
 from django.db.models import Q, Sum
-from django.db.models.functions import Lower
 from django.utils.translation import gettext_lazy as _
 
 from dcim.models import BaseInterface
@@ -70,7 +69,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     name = models.CharField(
         verbose_name=_('name'),
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     status = models.CharField(
         max_length=50,
@@ -156,11 +155,11 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
         ordering = ('name', 'pk')  # Name may be non-unique
         constraints = (
             models.UniqueConstraint(
-                Lower('name'), 'cluster', 'tenant',
+                'name', 'cluster', 'tenant',
                 name='%(app_label)s_%(class)s_unique_name_cluster_tenant'
             ),
             models.UniqueConstraint(
-                Lower('name'), 'cluster',
+                '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.")
@@ -275,7 +274,7 @@ class ComponentModel(NetBoxModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=64,
-        db_collation="natural_sort"
+        db_collation='ci_natural_sort',
     )
     description = models.CharField(
         verbose_name=_('description'),

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

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

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

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

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

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

+ 3 - 2
netbox/wireless/models.py

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