ソースを参照

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'),
         max_length=100,
         unique=True,
-        help_text=_('Full name of the provider')
+        help_text=_('Full name of the provider'),
+        db_collation="natural_sort"
     )
     slug = models.SlugField(
         verbose_name=_('slug'),
@@ -95,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
     """
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation="natural_sort"
     )
     provider = models.ForeignKey(
         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['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):
         site = Site(name='Site 1', slug='site-1')
         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['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):
         site = Site(
             name='Site 1',
@@ -153,10 +143,6 @@ class ChangeLogViewTest(ModelViewTestCase):
         self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
         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):
         sites = (
             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['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):
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -389,12 +371,6 @@ class ChangeLogAPITest(APITestCase):
         self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
         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):
         site = Site(
             name='Site 1',
@@ -423,10 +399,6 @@ class ChangeLogAPITest(APITestCase):
         self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
         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):
         data = (
             {

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

@@ -76,7 +76,6 @@ class ComponentType(
     """
     Base type for device/VM components
     """
-    _name: str
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
 
 
@@ -93,7 +92,6 @@ class ComponentTemplateType(
     """
     Base type for device/VM components
     """
-    _name: str
     device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]
 
 
@@ -181,7 +179,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
     filters=ConsolePortTemplateFilter
 )
 class ConsolePortTemplateType(ModularComponentTemplateType):
-    _name: str
+    pass
 
 
 @strawberry_django.type(
@@ -199,7 +197,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
     filters=ConsoleServerPortTemplateFilter
 )
 class ConsoleServerPortTemplateType(ModularComponentTemplateType):
-    _name: str
+    pass
 
 
 @strawberry_django.type(
@@ -208,7 +206,6 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
     filters=DeviceFilter
 )
 class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     console_port_count: BigInt
     console_server_port_count: BigInt
     power_port_count: BigInt
@@ -273,7 +270,7 @@ class DeviceBayType(ComponentType):
     filters=DeviceBayTemplateFilter
 )
 class DeviceBayTemplateType(ComponentTemplateType):
-    _name: str
+    pass
 
 
 @strawberry_django.type(
@@ -282,7 +279,6 @@ class DeviceBayTemplateType(ComponentTemplateType):
     filters=InventoryItemTemplateFilter
 )
 class InventoryItemTemplateType(ComponentTemplateType):
-    _name: str
     role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
 
@@ -366,7 +362,6 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
     filters=FrontPortTemplateFilter
 )
 class FrontPortTemplateType(ModularComponentTemplateType):
-    _name: str
     color: str
     rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
 
@@ -377,6 +372,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
     filters=InterfaceFilter
 )
 class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
+    _name: str
     mac_address: str | None
     wwn: str | None
     parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
@@ -527,7 +523,7 @@ class ModuleBayType(ModularComponentType):
     filters=ModuleBayTemplateFilter
 )
 class ModuleBayTemplateType(ModularComponentTemplateType):
-    _name: str
+    pass
 
 
 @strawberry_django.type(
@@ -588,7 +584,6 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
     filters=PowerOutletTemplateFilter
 )
 class PowerOutletTemplateType(ModularComponentTemplateType):
-    _name: str
     power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
 
 
@@ -620,8 +615,6 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
     filters=PowerPortTemplateFilter
 )
 class PowerPortTemplateType(ModularComponentTemplateType):
-    _name: str
-
     poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
 
 
@@ -640,7 +633,6 @@ class RackTypeType(NetBoxObjectType):
     filters=RackFilter
 )
 class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -693,7 +685,6 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
     filters=RearPortTemplateFilter
 )
 class RearPortTemplateType(ModularComponentTemplateType):
-    _name: str
     color: str
 
     frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@@ -729,7 +720,6 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
     filters=SiteFilter
 )
 class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
-    _name: str
     time_zone: str | None
     region: Annotated["RegionType", 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,
         help_text=_(
             "{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(
         verbose_name=_('label'),
@@ -65,7 +61,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
 
     class Meta:
         abstract = True
-        ordering = ('device_type', '_name')
+        ordering = ('device_type', 'name')
         constraints = (
             models.UniqueConstraint(
                 fields=('device_type', 'name'),
@@ -125,7 +121,7 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
 
     class Meta:
         abstract = True
-        ordering = ('device_type', 'module_type', '_name')
+        ordering = ('device_type', 'module_type', 'name')
         constraints = (
             models.UniqueConstraint(
                 fields=('device_type', 'name'),
@@ -782,7 +778,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
     component_model = InventoryItem
 
     class Meta:
-        ordering = ('device_type__id', 'parent__id', '_name')
+        ordering = ('device_type__id', 'parent__id', 'name')
         indexes = (
             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(
         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(
         verbose_name=_('label'),
@@ -71,7 +67,7 @@ class ComponentModel(NetBoxModel):
 
     class Meta:
         abstract = True
-        ordering = ('device', '_name')
+        ordering = ('device', 'name')
         constraints = (
             models.UniqueConstraint(
                 fields=('device', 'name'),
@@ -1301,7 +1297,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
     clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
 
     class Meta:
-        ordering = ('device__id', 'parent__id', '_name')
+        ordering = ('device__id', 'parent__id', 'name')
         indexes = (
             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.mixins import WeightMixin
 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 .device_components import *
 from .mixins import RenderConfigMixin
@@ -582,13 +582,8 @@ class Device(
         verbose_name=_('name'),
         max_length=64,
         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(
         max_length=50,
@@ -775,7 +770,7 @@ class Device(
     )
 
     class Meta:
-        ordering = ('_name', 'pk')  # Name may be null
+        ordering = ('name', 'pk')  # Name may be null
         constraints = (
             models.UniqueConstraint(
                 Lower('name'), 'site', 'tenant',
@@ -1320,7 +1315,8 @@ class VirtualChassis(PrimaryModel):
     )
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=64
+        max_length=64,
+        db_collation="natural_sort"
     )
     domain = models.CharField(
         verbose_name=_('domain'),
@@ -1382,7 +1378,8 @@ class VirtualDeviceContext(PrimaryModel):
     )
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=64
+        max_length=64,
+        db_collation="natural_sort"
     )
     status = models.CharField(
         verbose_name=_('status'),

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

@@ -36,7 +36,8 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
     )
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation="natural_sort"
     )
 
     prerequisite_models = (
@@ -86,7 +87,8 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
     )
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation="natural_sort"
     )
     status = models.CharField(
         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 utilities.conversion import to_grams
 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 .devices import Device, Module
 from .power import PowerFeed
@@ -255,12 +255,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
     )
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=100
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
         max_length=100,
-        blank=True
+        db_collation="natural_sort"
     )
     facility_id = models.CharField(
         max_length=50,
@@ -340,7 +336,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
     )
 
     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 = (
             # Name and facility_id must be unique *only* within a Location
             models.UniqueConstraint(

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

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

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

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

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

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

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

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

+ 3 - 4
netbox/dcim/views.py

@@ -688,8 +688,7 @@ class RackElevationListView(generic.ObjectListView):
         sort = request.GET.get('sort', 'name')
         if sort not in ORDERING_CHOICES:
             sort = 'name'
-        sort_field = sort.replace("name", "_name")  # Use natural ordering
-        racks = racks.order_by(sort_field)
+        racks = racks.order_by(sort)
 
         # Pagination
         per_page = get_paginate_count(request)
@@ -731,8 +730,8 @@ class RackView(GetRelatedModelsMixin, generic.ObjectView):
             peer_racks = peer_racks.filter(location=instance.location)
         else:
             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
         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(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     slug = models.SlugField(
         verbose_name=_('slug'),

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

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

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

@@ -18,7 +18,8 @@ class VRF(PrimaryModel):
     """
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation="natural_sort"
     )
     rd = models.CharField(
         max_length=VRF_RD_MAX_LENGTH,
@@ -74,7 +75,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,
-        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(
         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(
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation="natural_sort"
     )
     title = models.CharField(
         verbose_name=_('title'),

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

@@ -18,7 +18,8 @@ class TenantGroup(NestedGroupModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     slug = models.SlugField(
         verbose_name=_('slug'),
@@ -39,7 +40,8 @@ class Tenant(ContactsMixin, PrimaryModel):
     """
     name = models.CharField(
         verbose_name=_('name'),
-        max_length=100
+        max_length=100,
+        db_collation="natural_sort"
     )
     slug = models.SlugField(
         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.translation import gettext_lazy as _
 
-from utilities.ordering import naturalize
 from .forms.widgets import ColorSelect
 from .validators import ColorValidator
 
@@ -40,7 +39,7 @@ class NaturalOrderingField(models.CharField):
     """
     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.naturalize_function = naturalize_function
         super().__init__(*args, **kwargs)

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

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

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

@@ -69,12 +69,8 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     )
     name = models.CharField(
         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(
         max_length=50,
@@ -152,7 +148,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     )
 
     class Meta:
-        ordering = ('_name', 'pk')  # Name may be non-unique
+        ordering = ('name', 'pk')  # Name may be non-unique
         constraints = (
             models.UniqueConstraint(
                 Lower('name'), 'cluster', 'tenant',
@@ -273,13 +269,8 @@ class ComponentModel(NetBoxModel):
     )
     name = models.CharField(
         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(
         verbose_name=_('description'),
@@ -289,7 +280,6 @@ class ComponentModel(NetBoxModel):
 
     class Meta:
         abstract = True
-        ordering = ('virtual_machine', CollateAsChar('_name'))
         constraints = (
             models.UniqueConstraint(
                 fields=('virtual_machine', 'name'),
@@ -311,10 +301,9 @@ class ComponentModel(NetBoxModel):
 
 
 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(
         target_field='name',
@@ -322,6 +311,11 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
         max_length=100,
         blank=True
     )
+    virtual_machine = models.ForeignKey(
+        to='virtualization.VirtualMachine',
+        on_delete=models.CASCADE,
+        related_name='interfaces'  # Override ComponentModel
+    )
     ip_addresses = GenericRelation(
         to='ipam.IPAddress',
         content_type_field='assigned_object_type',
@@ -358,6 +352,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
     class Meta(ComponentModel.Meta):
         verbose_name = _('interface')
         verbose_name_plural = _('interfaces')
+        ordering = ('virtual_machine', CollateAsChar('_name'))
 
     def clean(self):
         super().clean()
@@ -416,3 +411,4 @@ class VirtualDisk(ComponentModel, TrackingModelMixin):
     class Meta(ComponentModel.Meta):
         verbose_name = _('virtual disk')
         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):
     name = tables.Column(
         verbose_name=_('Name'),
-        order_by=('_name',),
         linkify=True
     )
     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(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     authentication_method = models.CharField(
         verbose_name=('authentication method'),
@@ -67,7 +68,8 @@ class IKEPolicy(PrimaryModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     version = models.PositiveSmallIntegerField(
         verbose_name=_('version'),
@@ -125,7 +127,8 @@ class IPSecProposal(PrimaryModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     encryption_algorithm = models.CharField(
         verbose_name=_('encryption'),
@@ -176,7 +179,8 @@ class IPSecPolicy(PrimaryModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     proposals = models.ManyToManyField(
         to='vpn.IPSecProposal',
@@ -211,7 +215,8 @@ class IPSecProfile(PrimaryModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     mode = models.CharField(
         verbose_name=_('mode'),

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

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

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

@@ -31,7 +31,8 @@ class Tunnel(PrimaryModel):
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     status = models.CharField(
         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(
         verbose_name=_('name'),
         max_length=100,
-        unique=True
+        unique=True,
+        db_collation="natural_sort"
     )
     slug = models.SlugField(
         verbose_name=_('slug'),