فهرست منبع

Closes #11279: Replace `_name` natural key sorting with collation (#18009)

* 11279 add collation

* 11279 add collation

* 11279 add collation

* 11279 add collation

* 11279 fix tables /tests

* 11279 fix tests

* 11279 refactor VirtualDisk

* Clean up migrations

* Misc cleanup

* Correct errant file inclusion

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Arthur Hanson 1 سال پیش
والد
کامیت
6ab0792f02
35فایلهای تغییر یافته به همراه622 افزوده شده و 150 حذف شده
  1. 22 0
      netbox/circuits/migrations/0049_natural_ordering.py
  2. 4 2
      netbox/circuits/models/providers.py
  3. 0 28
      netbox/core/tests/test_changelog.py
  4. 5 15
      netbox/dcim/graphql/types.py
  5. 17 0
      netbox/dcim/migrations/0197_natural_sort_collation.py
  6. 318 0
      netbox/dcim/migrations/0198_natural_ordering.py
  7. 5 9
      netbox/dcim/models/device_component_templates.py
  8. 4 8
      netbox/dcim/models/device_components.py
  9. 8 11
      netbox/dcim/models/devices.py
  10. 4 2
      netbox/dcim/models/power.py
  11. 3 7
      netbox/dcim/models/racks.py
  12. 3 8
      netbox/dcim/models/sites.py
  13. 6 12
      netbox/dcim/tables/devices.py
  14. 5 3
      netbox/dcim/tables/devicetypes.py
  15. 0 1
      netbox/dcim/tables/racks.py
  16. 3 4
      netbox/dcim/views.py
  17. 32 0
      netbox/ipam/migrations/0076_natural_ordering.py
  18. 2 1
      netbox/ipam/models/asns.py
  19. 2 1
      netbox/ipam/models/vlans.py
  20. 4 2
      netbox/ipam/models/vrfs.py
  21. 27 0
      netbox/tenancy/migrations/0017_natural_ordering.py
  22. 2 1
      netbox/tenancy/models/contacts.py
  23. 4 2
      netbox/tenancy/models/tenants.py
  24. 1 2
      netbox/utilities/fields.py
  25. 1 2
      netbox/virtualization/graphql/types.py
  26. 43 0
      netbox/virtualization/migrations/0046_natural_ordering.py
  27. 2 1
      netbox/virtualization/models/clusters.py
  28. 15 19
      netbox/virtualization/models/virtualmachines.py
  29. 0 1
      netbox/virtualization/tables/virtualmachines.py
  30. 47 0
      netbox/vpn/migrations/0007_natural_ordering.py
  31. 10 5
      netbox/vpn/models/crypto.py
  32. 2 1
      netbox/vpn/models/l2vpn.py
  33. 2 1
      netbox/vpn/models/tunnels.py
  34. 17 0
      netbox/wireless/migrations/0012_natural_ordering.py
  35. 2 1
      netbox/wireless/models.py

+ 22 - 0
netbox/circuits/migrations/0049_natural_ordering.py

@@ -0,0 +1,22 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('circuits', '0048_circuitterminations_cached_relations'),
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='provider',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='providernetwork',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+    ]

+ 4 - 2
netbox/circuits/models/providers.py

@@ -21,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
         unique=True,
         unique=True,
-        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'),
@@ -95,7 +96,8 @@ 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"
     )
     )
     provider = models.ForeignKey(
     provider = models.ForeignKey(
         to='circuits.Provider',
         to='circuits.Provider',

+ 0 - 28
netbox/core/tests/test_changelog.py

@@ -76,10 +76,6 @@ class ChangeLogViewTest(ModelViewTestCase):
         self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
         self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
 
 
-        # Check that private attributes were included in raw data but not display data
-        self.assertIn('_name', oc.postchange_data)
-        self.assertNotIn('_name', oc.postchange_data_clean)
-
     def test_update_object(self):
     def test_update_object(self):
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -117,12 +113,6 @@ class ChangeLogViewTest(ModelViewTestCase):
         self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
         self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
 
 
-        # Check that private attributes were included in raw data but not display data
-        self.assertIn('_name', oc.prechange_data)
-        self.assertNotIn('_name', oc.prechange_data_clean)
-        self.assertIn('_name', oc.postchange_data)
-        self.assertNotIn('_name', oc.postchange_data_clean)
-
     def test_delete_object(self):
     def test_delete_object(self):
         site = Site(
         site = Site(
             name='Site 1',
             name='Site 1',
@@ -153,10 +143,6 @@ class ChangeLogViewTest(ModelViewTestCase):
         self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
         self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
         self.assertEqual(oc.postchange_data, None)
         self.assertEqual(oc.postchange_data, None)
 
 
-        # Check that private attributes were included in raw data but not display data
-        self.assertIn('_name', oc.prechange_data)
-        self.assertNotIn('_name', oc.prechange_data_clean)
-
     def test_bulk_update_objects(self):
     def test_bulk_update_objects(self):
         sites = (
         sites = (
             Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
             Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
@@ -353,10 +339,6 @@ class ChangeLogAPITest(APITestCase):
         self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
         self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
 
 
-        # Check that private attributes were included in raw data but not display data
-        self.assertIn('_name', oc.postchange_data)
-        self.assertNotIn('_name', oc.postchange_data_clean)
-
     def test_update_object(self):
     def test_update_object(self):
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
@@ -389,12 +371,6 @@ class ChangeLogAPITest(APITestCase):
         self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
         self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
         self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
 
 
-        # Check that private attributes were included in raw data but not display data
-        self.assertIn('_name', oc.prechange_data)
-        self.assertNotIn('_name', oc.prechange_data_clean)
-        self.assertIn('_name', oc.postchange_data)
-        self.assertNotIn('_name', oc.postchange_data_clean)
-
     def test_delete_object(self):
     def test_delete_object(self):
         site = Site(
         site = Site(
             name='Site 1',
             name='Site 1',
@@ -423,10 +399,6 @@ class ChangeLogAPITest(APITestCase):
         self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
         self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
         self.assertEqual(oc.postchange_data, None)
         self.assertEqual(oc.postchange_data, None)
 
 
-        # Check that private attributes were included in raw data but not display data
-        self.assertIn('_name', oc.prechange_data)
-        self.assertNotIn('_name', oc.prechange_data_clean)
-
     def test_bulk_create_objects(self):
     def test_bulk_create_objects(self):
         data = (
         data = (
             {
             {

+ 5 - 15
netbox/dcim/graphql/types.py

@@ -76,7 +76,6 @@ class ComponentType(
     """
     """
     Base type for device/VM components
     Base type for device/VM components
     """
     """
-    _name: str
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
 
 
 
 
@@ -93,7 +92,6 @@ class ComponentTemplateType(
     """
     """
     Base type for device/VM components
     Base type for device/VM components
     """
     """
-    _name: str
     device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]
     device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]
 
 
 
 
@@ -181,7 +179,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
     filters=ConsolePortTemplateFilter
     filters=ConsolePortTemplateFilter
 )
 )
 class ConsolePortTemplateType(ModularComponentTemplateType):
 class ConsolePortTemplateType(ModularComponentTemplateType):
-    _name: str
+    pass
 
 
 
 
 @strawberry_django.type(
 @strawberry_django.type(
@@ -199,7 +197,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
     filters=ConsoleServerPortTemplateFilter
     filters=ConsoleServerPortTemplateFilter
 )
 )
 class ConsoleServerPortTemplateType(ModularComponentTemplateType):
 class ConsoleServerPortTemplateType(ModularComponentTemplateType):
-    _name: str
+    pass
 
 
 
 
 @strawberry_django.type(
 @strawberry_django.type(
@@ -208,7 +206,6 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
     filters=DeviceFilter
     filters=DeviceFilter
 )
 )
 class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     console_port_count: BigInt
     console_port_count: BigInt
     console_server_port_count: BigInt
     console_server_port_count: BigInt
     power_port_count: BigInt
     power_port_count: BigInt
@@ -273,7 +270,7 @@ class DeviceBayType(ComponentType):
     filters=DeviceBayTemplateFilter
     filters=DeviceBayTemplateFilter
 )
 )
 class DeviceBayTemplateType(ComponentTemplateType):
 class DeviceBayTemplateType(ComponentTemplateType):
-    _name: str
+    pass
 
 
 
 
 @strawberry_django.type(
 @strawberry_django.type(
@@ -282,7 +279,6 @@ class DeviceBayTemplateType(ComponentTemplateType):
     filters=InventoryItemTemplateFilter
     filters=InventoryItemTemplateFilter
 )
 )
 class InventoryItemTemplateType(ComponentTemplateType):
 class InventoryItemTemplateType(ComponentTemplateType):
-    _name: str
     role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
     role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
 
 
@@ -366,7 +362,6 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
     filters=FrontPortTemplateFilter
     filters=FrontPortTemplateFilter
 )
 )
 class FrontPortTemplateType(ModularComponentTemplateType):
 class FrontPortTemplateType(ModularComponentTemplateType):
-    _name: str
     color: str
     color: str
     rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
     rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
 
 
@@ -377,6 +372,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
     filters=InterfaceFilter
     filters=InterfaceFilter
 )
 )
 class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
 class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
+    _name: str
     mac_address: str | None
     mac_address: str | None
     wwn: str | None
     wwn: str | None
     parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
     parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
@@ -527,7 +523,7 @@ class ModuleBayType(ModularComponentType):
     filters=ModuleBayTemplateFilter
     filters=ModuleBayTemplateFilter
 )
 )
 class ModuleBayTemplateType(ModularComponentTemplateType):
 class ModuleBayTemplateType(ModularComponentTemplateType):
-    _name: str
+    pass
 
 
 
 
 @strawberry_django.type(
 @strawberry_django.type(
@@ -588,7 +584,6 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
     filters=PowerOutletTemplateFilter
     filters=PowerOutletTemplateFilter
 )
 )
 class PowerOutletTemplateType(ModularComponentTemplateType):
 class PowerOutletTemplateType(ModularComponentTemplateType):
-    _name: str
     power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
     power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
 
 
 
 
@@ -620,8 +615,6 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
     filters=PowerPortTemplateFilter
     filters=PowerPortTemplateFilter
 )
 )
 class PowerPortTemplateType(ModularComponentTemplateType):
 class PowerPortTemplateType(ModularComponentTemplateType):
-    _name: str
-
     poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
     poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
 
 
 
 
@@ -640,7 +633,6 @@ class RackTypeType(NetBoxObjectType):
     filters=RackFilter
     filters=RackFilter
 )
 )
 class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -693,7 +685,6 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
     filters=RearPortTemplateFilter
     filters=RearPortTemplateFilter
 )
 )
 class RearPortTemplateType(ModularComponentTemplateType):
 class RearPortTemplateType(ModularComponentTemplateType):
-    _name: str
     color: str
     color: str
 
 
     frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
     frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@@ -729,7 +720,6 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
     filters=SiteFilter
     filters=SiteFilter
 )
 )
 class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     time_zone: str | None
     time_zone: str | None
     region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
     region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
     group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None
     group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None

+ 17 - 0
netbox/dcim/migrations/0197_natural_sort_collation.py

@@ -0,0 +1,17 @@
+from django.contrib.postgres.operations import CreateCollation
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0196_qinq_svlan'),
+    ]
+
+    operations = [
+        CreateCollation(
+            "natural_sort",
+            provider="icu",
+            locale="und-u-kn-true",
+        ),
+    ]

+ 318 - 0
netbox/dcim/migrations/0198_natural_ordering.py

@@ -0,0 +1,318 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='site',
+            options={'ordering': ('name',)},
+        ),
+        migrations.AlterField(
+            model_name='site',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterModelOptions(
+            name='consoleport',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='consoleporttemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='consoleserverport',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='consoleserverporttemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='device',
+            options={'ordering': ('name', 'pk')},
+        ),
+        migrations.AlterModelOptions(
+            name='devicebay',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='devicebaytemplate',
+            options={'ordering': ('device_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='frontport',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='frontporttemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='interfacetemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='inventoryitem',
+            options={'ordering': ('device__id', 'parent__id', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='inventoryitemtemplate',
+            options={'ordering': ('device_type__id', 'parent__id', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='modulebay',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='modulebaytemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='poweroutlet',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='poweroutlettemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='powerport',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='powerporttemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='rack',
+            options={'ordering': ('site', 'location', 'name', 'pk')},
+        ),
+        migrations.AlterModelOptions(
+            name='rearport',
+            options={'ordering': ('device', 'name')},
+        ),
+        migrations.AlterModelOptions(
+            name='rearporttemplate',
+            options={'ordering': ('device_type', 'module_type', 'name')},
+        ),
+        migrations.RemoveField(
+            model_name='consoleport',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='consoleporttemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='consoleserverport',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='consoleserverporttemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='device',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='devicebay',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='devicebaytemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='frontport',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='frontporttemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='inventoryitem',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='inventoryitemtemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='modulebay',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='modulebaytemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='poweroutlet',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='poweroutlettemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='powerport',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='powerporttemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='rack',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='rearport',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='rearporttemplate',
+            name='_name',
+        ),
+        migrations.RemoveField(
+            model_name='site',
+            name='_name',
+        ),
+        migrations.AlterField(
+            model_name='consoleport',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleporttemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverport',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverporttemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='device',
+            name='name',
+            field=models.CharField(blank=True, db_collation='natural_sort', max_length=64, null=True),
+        ),
+        migrations.AlterField(
+            model_name='devicebay',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='devicebaytemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='frontport',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='frontporttemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='interface',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='interfacetemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitemtemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='modulebay',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='modulebaytemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlet',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerport',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerporttemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='rack',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.AlterField(
+            model_name='rearport',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='rearporttemplate',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerfeed',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.AlterField(
+            model_name='powerpanel',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.AlterField(
+            model_name='virtualchassis',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='virtualdevicecontext',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+    ]

+ 5 - 9
netbox/dcim/models/device_component_templates.py

@@ -44,12 +44,8 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
         max_length=64,
         max_length=64,
         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."
-        )
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
+        ),
+        db_collation="natural_sort"
     )
     )
     label = models.CharField(
     label = models.CharField(
         verbose_name=_('label'),
         verbose_name=_('label'),
@@ -65,7 +61,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
 
 
     class Meta:
     class Meta:
         abstract = True
         abstract = True
-        ordering = ('device_type', '_name')
+        ordering = ('device_type', 'name')
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('device_type', 'name'),
                 fields=('device_type', 'name'),
@@ -125,7 +121,7 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
 
 
     class Meta:
     class Meta:
         abstract = True
         abstract = True
-        ordering = ('device_type', 'module_type', '_name')
+        ordering = ('device_type', 'module_type', 'name')
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('device_type', 'name'),
                 fields=('device_type', 'name'),
@@ -782,7 +778,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
     component_model = InventoryItem
     component_model = InventoryItem
 
 
     class Meta:
     class Meta:
-        ordering = ('device_type__id', 'parent__id', '_name')
+        ordering = ('device_type__id', 'parent__id', 'name')
         indexes = (
         indexes = (
             models.Index(fields=('component_type', 'component_id')),
             models.Index(fields=('component_type', 'component_id')),
         )
         )

+ 4 - 8
netbox/dcim/models/device_components.py

@@ -50,12 +50,8 @@ class ComponentModel(NetBoxModel):
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
+        max_length=64,
+        db_collation="natural_sort"
     )
     )
     label = models.CharField(
     label = models.CharField(
         verbose_name=_('label'),
         verbose_name=_('label'),
@@ -71,7 +67,7 @@ class ComponentModel(NetBoxModel):
 
 
     class Meta:
     class Meta:
         abstract = True
         abstract = True
-        ordering = ('device', '_name')
+        ordering = ('device', 'name')
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('device', 'name'),
                 fields=('device', 'name'),
@@ -1301,7 +1297,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
     clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
     clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
 
 
     class Meta:
     class Meta:
-        ordering = ('device__id', 'parent__id', '_name')
+        ordering = ('device__id', 'parent__id', 'name')
         indexes = (
         indexes = (
             models.Index(fields=('component_type', 'component_id')),
             models.Index(fields=('component_type', 'component_id')),
         )
         )

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

@@ -23,7 +23,7 @@ from netbox.config import ConfigItem
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models.mixins import WeightMixin
 from netbox.models.mixins import WeightMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
-from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
+from utilities.fields import ColorField, CounterCacheField
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin
 from .device_components import *
 from .device_components import *
 from .mixins import RenderConfigMixin
 from .mixins import RenderConfigMixin
@@ -582,13 +582,8 @@ class Device(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=64,
         max_length=64,
         blank=True,
         blank=True,
-        null=True
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True,
-        null=True
+        null=True,
+        db_collation="natural_sort"
     )
     )
     serial = models.CharField(
     serial = models.CharField(
         max_length=50,
         max_length=50,
@@ -775,7 +770,7 @@ class Device(
     )
     )
 
 
     class Meta:
     class Meta:
-        ordering = ('_name', 'pk')  # Name may be null
+        ordering = ('name', 'pk')  # Name may be null
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 Lower('name'), 'site', 'tenant',
                 Lower('name'), 'site', 'tenant',
@@ -1320,7 +1315,8 @@ 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"
     )
     )
     domain = models.CharField(
     domain = models.CharField(
         verbose_name=_('domain'),
         verbose_name=_('domain'),
@@ -1382,7 +1378,8 @@ 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"
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

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

@@ -36,7 +36,8 @@ 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"
     )
     )
 
 
     prerequisite_models = (
     prerequisite_models = (
@@ -86,7 +87,8 @@ 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"
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

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

@@ -19,7 +19,7 @@ from netbox.models.mixins import WeightMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.conversion import to_grams
 from utilities.conversion import to_grams
 from utilities.data import array_to_string, drange
 from utilities.data import array_to_string, drange
-from utilities.fields import ColorField, NaturalOrderingField
+from utilities.fields import ColorField
 from .device_components import PowerPort
 from .device_components import PowerPort
 from .devices import Device, Module
 from .devices import Device, Module
 from .power import PowerFeed
 from .power import PowerFeed
@@ -255,12 +255,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
-        max_length=100
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
         max_length=100,
         max_length=100,
-        blank=True
+        db_collation="natural_sort"
     )
     )
     facility_id = models.CharField(
     facility_id = models.CharField(
         max_length=50,
         max_length=50,
@@ -340,7 +336,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
     )
     )
 
 
     class Meta:
     class Meta:
-        ordering = ('site', 'location', '_name', 'pk')  # (site, location, name) may be non-unique
+        ordering = ('site', 'location', 'name', 'pk')  # (site, location, name) may be non-unique
         constraints = (
         constraints = (
             # Name and facility_id must be unique *only* within a Location
             # Name and facility_id must be unique *only* within a Location
             models.UniqueConstraint(
             models.UniqueConstraint(

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

@@ -8,7 +8,6 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
-from utilities.fields import NaturalOrderingField
 
 
 __all__ = (
 __all__ = (
     'Location',
     'Location',
@@ -143,12 +142,8 @@ 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")
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
+        help_text=_("Full name of the site"),
+        db_collation="natural_sort"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
@@ -245,7 +240,7 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
     )
     )
 
 
     class Meta:
     class Meta:
-        ordering = ('_name',)
+        ordering = ('name',)
         verbose_name = _('site')
         verbose_name = _('site')
         verbose_name_plural = _('sites')
         verbose_name_plural = _('sites')
 
 

+ 6 - 12
netbox/dcim/tables/devices.py

@@ -132,7 +132,6 @@ class PlatformTable(NetBoxTable):
 class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
 class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
     name = tables.TemplateColumn(
     name = tables.TemplateColumn(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
-        order_by=('_name',),
         template_code=DEVICE_LINK,
         template_code=DEVICE_LINK,
         linkify=True
         linkify=True
     )
     )
@@ -288,7 +287,6 @@ class DeviceComponentTable(NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True,
         linkify=True,
-        order_by=('_name',)
     )
     )
     device_status = columns.ChoiceFieldColumn(
     device_status = columns.ChoiceFieldColumn(
         accessor=tables.A('device__status'),
         accessor=tables.A('device__status'),
@@ -391,7 +389,6 @@ class DeviceConsolePortTable(ConsolePortTable):
     name = tables.TemplateColumn(
     name = tables.TemplateColumn(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
         template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -433,7 +430,6 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-console-network-outline"></i> '
         template_code='<i class="mdi mdi-console-network-outline"></i> '
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -482,7 +478,6 @@ class DevicePowerPortTable(PowerPortTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
         template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
                       '{{ value }}</a>',
                       '{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -531,7 +526,6 @@ class DevicePowerOutletTable(PowerOutletTable):
     name = tables.TemplateColumn(
     name = tables.TemplateColumn(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
         template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -550,6 +544,11 @@ class DevicePowerOutletTable(PowerOutletTable):
 
 
 
 
 class BaseInterfaceTable(NetBoxTable):
 class BaseInterfaceTable(NetBoxTable):
+    name = tables.Column(
+        verbose_name=_('Name'),
+        linkify=True,
+        order_by=('_name',)
+    )
     enabled = columns.BooleanColumn(
     enabled = columns.BooleanColumn(
         verbose_name=_('Enabled'),
         verbose_name=_('Enabled'),
     )
     )
@@ -597,7 +596,7 @@ class BaseInterfaceTable(NetBoxTable):
         return ",".join([str(obj) for obj in value.all()])
         return ",".join([str(obj) for obj in value.all()])
 
 
 
 
-class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
+class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpointTable):
     device = tables.Column(
     device = tables.Column(
         verbose_name=_('Device'),
         verbose_name=_('Device'),
         linkify={
         linkify={
@@ -736,7 +735,6 @@ class DeviceFrontPortTable(FrontPortTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
         template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -783,7 +781,6 @@ class DeviceRearPortTable(RearPortTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
         template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
                       '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -846,7 +843,6 @@ class DeviceDeviceBayTable(DeviceBayTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
         template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
                       '"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
                       '"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
@@ -915,7 +911,6 @@ class DeviceModuleBayTable(ModuleBayTable):
     name = columns.MPTTColumn(
     name = columns.MPTTColumn(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True,
         linkify=True,
-        order_by=Accessor('_name')
     )
     )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
         extra_buttons=MODULEBAY_BUTTONS
         extra_buttons=MODULEBAY_BUTTONS
@@ -982,7 +977,6 @@ class DeviceInventoryItemTable(InventoryItemTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
         template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
                       '{{ value }}</a>',
                       '{{ value }}</a>',
-        order_by=Accessor('_name'),
         attrs={'td': {'class': 'text-nowrap'}}
         attrs={'td': {'class': 'text-nowrap'}}
     )
     )
 
 

+ 5 - 3
netbox/dcim/tables/devicetypes.py

@@ -163,9 +163,7 @@ class ComponentTemplateTable(NetBoxTable):
     id = tables.Column(
     id = tables.Column(
         verbose_name=_('ID')
         verbose_name=_('ID')
     )
     )
-    name = tables.Column(
-        order_by=('_name',)
-    )
+    name = tables.Column()
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         exclude = ('id', )
         exclude = ('id', )
@@ -220,6 +218,10 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
 
 
 
 
 class InterfaceTemplateTable(ComponentTemplateTable):
 class InterfaceTemplateTable(ComponentTemplateTable):
+    name = tables.Column(
+        verbose_name=_('Name'),
+        order_by=('_name',)
+    )
     enabled = columns.BooleanColumn(
     enabled = columns.BooleanColumn(
         verbose_name=_('Enabled'),
         verbose_name=_('Enabled'),
     )
     )

+ 0 - 1
netbox/dcim/tables/racks.py

@@ -111,7 +111,6 @@ class RackTypeTable(NetBoxTable):
 class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
 class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
-        order_by=('_name',),
         linkify=True
         linkify=True
     )
     )
     location = tables.Column(
     location = tables.Column(

+ 3 - 4
netbox/dcim/views.py

@@ -688,8 +688,7 @@ class RackElevationListView(generic.ObjectListView):
         sort = request.GET.get('sort', 'name')
         sort = request.GET.get('sort', 'name')
         if sort not in ORDERING_CHOICES:
         if sort not in ORDERING_CHOICES:
             sort = 'name'
             sort = 'name'
-        sort_field = sort.replace("name", "_name")  # Use natural ordering
-        racks = racks.order_by(sort_field)
+        racks = racks.order_by(sort)
 
 
         # Pagination
         # Pagination
         per_page = get_paginate_count(request)
         per_page = get_paginate_count(request)
@@ -731,8 +730,8 @@ class RackView(GetRelatedModelsMixin, generic.ObjectView):
             peer_racks = peer_racks.filter(location=instance.location)
             peer_racks = peer_racks.filter(location=instance.location)
         else:
         else:
             peer_racks = peer_racks.filter(location__isnull=True)
             peer_racks = peer_racks.filter(location__isnull=True)
-        next_rack = peer_racks.filter(_name__gt=instance._name).first()
-        prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first()
+        next_rack = peer_racks.filter(name__gt=instance.name).first()
+        prev_rack = peer_racks.filter(name__lt=instance.name).reverse().first()
 
 
         # Determine any additional parameters to pass when embedding the rack elevations
         # Determine any additional parameters to pass when embedding the rack elevations
         svg_extra = '&'.join([
         svg_extra = '&'.join([

+ 32 - 0
netbox/ipam/migrations/0076_natural_ordering.py

@@ -0,0 +1,32 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0075_vlan_qinq'),
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='asnrange',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='routetarget',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=21, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='vlangroup',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.AlterField(
+            model_name='vrf',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+    ]

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

@@ -16,7 +16,8 @@ class ASNRange(OrganizationalModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),

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

@@ -35,7 +35,8 @@ 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"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),

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

@@ -18,7 +18,8 @@ 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"
     )
     )
     rd = models.CharField(
     rd = models.CharField(
         max_length=VRF_RD_MAX_LENGTH,
         max_length=VRF_RD_MAX_LENGTH,
@@ -74,7 +75,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,
-        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',

+ 27 - 0
netbox/tenancy/migrations/0017_natural_ordering.py

@@ -0,0 +1,27 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0016_charfield_null_choices'),
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contact',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.AlterField(
+            model_name='tenant',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.AlterField(
+            model_name='tenantgroup',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+    ]

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

@@ -56,7 +56,8 @@ 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"
     )
     )
     title = models.CharField(
     title = models.CharField(
         verbose_name=_('title'),
         verbose_name=_('title'),

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

@@ -18,7 +18,8 @@ class TenantGroup(NestedGroupModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),
@@ -39,7 +40,8 @@ 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"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),

+ 1 - 2
netbox/utilities/fields.py

@@ -5,7 +5,6 @@ from django.db import models
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
-from utilities.ordering import naturalize
 from .forms.widgets import ColorSelect
 from .forms.widgets import ColorSelect
 from .validators import ColorValidator
 from .validators import ColorValidator
 
 
@@ -40,7 +39,7 @@ class NaturalOrderingField(models.CharField):
     """
     """
     description = "Stores a representation of its target field suitable for natural ordering"
     description = "Stores a representation of its target field suitable for natural ordering"
 
 
-    def __init__(self, target_field, naturalize_function=naturalize, *args, **kwargs):
+    def __init__(self, target_field, naturalize_function, *args, **kwargs):
         self.target_field = target_field
         self.target_field = target_field
         self.naturalize_function = naturalize_function
         self.naturalize_function = naturalize_function
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)

+ 1 - 2
netbox/virtualization/graphql/types.py

@@ -25,7 +25,6 @@ class ComponentType(NetBoxObjectType):
     """
     """
     Base type for device/VM components
     Base type for device/VM components
     """
     """
-    _name: str
     virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]
     virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]
 
 
 
 
@@ -77,7 +76,6 @@ class ClusterTypeType(OrganizationalObjectType):
     filters=VirtualMachineFilter
     filters=VirtualMachineFilter
 )
 )
 class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
 class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     interface_count: BigInt
     interface_count: BigInt
     virtual_disk_count: BigInt
     virtual_disk_count: BigInt
     interface_count: BigInt
     interface_count: BigInt
@@ -102,6 +100,7 @@ class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
     filters=VMInterfaceFilter
     filters=VMInterfaceFilter
 )
 )
 class VMInterfaceType(IPAddressesMixin, ComponentType):
 class VMInterfaceType(IPAddressesMixin, ComponentType):
+    _name: str
     mac_address: str | None
     mac_address: str | None
     parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
     parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
     bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
     bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None

+ 43 - 0
netbox/virtualization/migrations/0046_natural_ordering.py

@@ -0,0 +1,43 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('virtualization', '0045_clusters_cached_relations'),
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='virtualmachine',
+            options={'ordering': ('name', 'pk')},
+        ),
+        migrations.AlterModelOptions(
+            name='virtualdisk',
+            options={'ordering': ('virtual_machine', 'name')},
+        ),
+        migrations.RemoveField(
+            model_name='virtualmachine',
+            name='_name',
+        ),
+        migrations.AlterField(
+            model_name='virtualdisk',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='virtualmachine',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='cluster',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100),
+        ),
+        migrations.RemoveField(
+            model_name='virtualdisk',
+            name='_name',
+        ),
+    ]

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

@@ -50,7 +50,8 @@ 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"
     )
     )
     type = models.ForeignKey(
     type = models.ForeignKey(
         verbose_name=_('type'),
         verbose_name=_('type'),

+ 15 - 19
netbox/virtualization/models/virtualmachines.py

@@ -69,12 +69,8 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
+        max_length=64,
+        db_collation="natural_sort"
     )
     )
     status = models.CharField(
     status = models.CharField(
         max_length=50,
         max_length=50,
@@ -152,7 +148,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     )
     )
 
 
     class Meta:
     class Meta:
-        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',
                 Lower('name'), 'cluster', 'tenant',
@@ -273,13 +269,8 @@ class ComponentModel(NetBoxModel):
     )
     )
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        naturalize_function=naturalize_interface,
-        max_length=100,
-        blank=True
+        max_length=64,
+        db_collation="natural_sort"
     )
     )
     description = models.CharField(
     description = models.CharField(
         verbose_name=_('description'),
         verbose_name=_('description'),
@@ -289,7 +280,6 @@ class ComponentModel(NetBoxModel):
 
 
     class Meta:
     class Meta:
         abstract = True
         abstract = True
-        ordering = ('virtual_machine', CollateAsChar('_name'))
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('virtual_machine', 'name'),
                 fields=('virtual_machine', 'name'),
@@ -311,10 +301,9 @@ class ComponentModel(NetBoxModel):
 
 
 
 
 class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
 class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
-    virtual_machine = models.ForeignKey(
-        to='virtualization.VirtualMachine',
-        on_delete=models.CASCADE,
-        related_name='interfaces'  # Override ComponentModel
+    name = models.CharField(
+        verbose_name=_('name'),
+        max_length=64,
     )
     )
     _name = NaturalOrderingField(
     _name = NaturalOrderingField(
         target_field='name',
         target_field='name',
@@ -322,6 +311,11 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
         max_length=100,
         max_length=100,
         blank=True
         blank=True
     )
     )
+    virtual_machine = models.ForeignKey(
+        to='virtualization.VirtualMachine',
+        on_delete=models.CASCADE,
+        related_name='interfaces'  # Override ComponentModel
+    )
     ip_addresses = GenericRelation(
     ip_addresses = GenericRelation(
         to='ipam.IPAddress',
         to='ipam.IPAddress',
         content_type_field='assigned_object_type',
         content_type_field='assigned_object_type',
@@ -358,6 +352,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
     class Meta(ComponentModel.Meta):
     class Meta(ComponentModel.Meta):
         verbose_name = _('interface')
         verbose_name = _('interface')
         verbose_name_plural = _('interfaces')
         verbose_name_plural = _('interfaces')
+        ordering = ('virtual_machine', CollateAsChar('_name'))
 
 
     def clean(self):
     def clean(self):
         super().clean()
         super().clean()
@@ -416,3 +411,4 @@ class VirtualDisk(ComponentModel, TrackingModelMixin):
     class Meta(ComponentModel.Meta):
     class Meta(ComponentModel.Meta):
         verbose_name = _('virtual disk')
         verbose_name = _('virtual disk')
         verbose_name_plural = _('virtual disks')
         verbose_name_plural = _('virtual disks')
+        ordering = ('virtual_machine', 'name')

+ 0 - 1
netbox/virtualization/tables/virtualmachines.py

@@ -53,7 +53,6 @@ VMINTERFACE_BUTTONS = """
 class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
 class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
-        order_by=('_name',),
         linkify=True
         linkify=True
     )
     )
     status = columns.ChoiceFieldColumn(
     status = columns.ChoiceFieldColumn(

+ 47 - 0
netbox/vpn/migrations/0007_natural_ordering.py

@@ -0,0 +1,47 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('vpn', '0006_charfield_null_choices'),
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='ikepolicy',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='ikeproposal',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='ipsecpolicy',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='ipsecprofile',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='ipsecproposal',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='l2vpn',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='tunnel',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+    ]

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

@@ -22,7 +22,8 @@ class IKEProposal(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="natural_sort"
     )
     )
     authentication_method = models.CharField(
     authentication_method = models.CharField(
         verbose_name=('authentication method'),
         verbose_name=('authentication method'),
@@ -67,7 +68,8 @@ class IKEPolicy(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="natural_sort"
     )
     )
     version = models.PositiveSmallIntegerField(
     version = models.PositiveSmallIntegerField(
         verbose_name=_('version'),
         verbose_name=_('version'),
@@ -125,7 +127,8 @@ class IPSecProposal(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="natural_sort"
     )
     )
     encryption_algorithm = models.CharField(
     encryption_algorithm = models.CharField(
         verbose_name=_('encryption'),
         verbose_name=_('encryption'),
@@ -176,7 +179,8 @@ class IPSecPolicy(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="natural_sort"
     )
     )
     proposals = models.ManyToManyField(
     proposals = models.ManyToManyField(
         to='vpn.IPSecProposal',
         to='vpn.IPSecProposal',
@@ -211,7 +215,8 @@ class IPSecProfile(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="natural_sort"
     )
     )
     mode = models.CharField(
     mode = models.CharField(
         verbose_name=_('mode'),
         verbose_name=_('mode'),

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

@@ -20,7 +20,8 @@ class L2VPN(ContactsMixin, 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="natural_sort"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),

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

@@ -31,7 +31,8 @@ class Tunnel(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="natural_sort"
     )
     )
     status = models.CharField(
     status = models.CharField(
         verbose_name=_('status'),
         verbose_name=_('status'),

+ 17 - 0
netbox/wireless/migrations/0012_natural_ordering.py

@@ -0,0 +1,17 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wireless', '0011_wirelesslan__location_wirelesslan__region_and_more'),
+        ('dcim', '0197_natural_sort_collation'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='wirelesslangroup',
+            name='name',
+            field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
+        ),
+    ]

+ 2 - 1
netbox/wireless/models.py

@@ -52,7 +52,8 @@ class WirelessLANGroup(NestedGroupModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         verbose_name=_('slug'),
         verbose_name=_('slug'),