diff --git a/netbox/circuits/migrations/0049_natural_ordering.py b/netbox/circuits/migrations/0049_natural_ordering.py new file mode 100644 index 000000000..1b4f565e8 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index f0fe77b1a..be81caa54 100644 --- a/netbox/circuits/models/providers.py +++ b/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', diff --git a/netbox/core/tests/test_changelog.py b/netbox/core/tests/test_changelog.py index c58968ee8..4914dbaf3 100644 --- a/netbox/core/tests/test_changelog.py +++ b/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 = ( { diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 597002acd..3642100a8 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -77,7 +77,6 @@ class ComponentType( """ Base type for device/VM components """ - _name: str device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] @@ -94,7 +93,6 @@ class ComponentTemplateType( """ Base type for device/VM components """ - _name: str device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')] @@ -182,7 +180,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin filters=ConsolePortTemplateFilter ) class ConsolePortTemplateType(ModularComponentTemplateType): - _name: str + pass @strawberry_django.type( @@ -200,7 +198,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin filters=ConsoleServerPortTemplateFilter ) class ConsoleServerPortTemplateType(ModularComponentTemplateType): - _name: str + pass @strawberry_django.type( @@ -209,7 +207,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 @@ -274,7 +271,7 @@ class DeviceBayType(ComponentType): filters=DeviceBayTemplateFilter ) class DeviceBayTemplateType(ComponentTemplateType): - _name: str + pass @strawberry_django.type( @@ -283,7 +280,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')] @@ -367,7 +363,6 @@ class FrontPortType(ModularComponentType, CabledObjectMixin): filters=FrontPortTemplateFilter ) class FrontPortTemplateType(ModularComponentTemplateType): - _name: str color: str rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')] @@ -387,6 +382,8 @@ class MACAddressType(NetBoxObjectType): 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 bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None @@ -537,7 +534,7 @@ class ModuleBayType(ModularComponentType): filters=ModuleBayTemplateFilter ) class ModuleBayTemplateType(ModularComponentTemplateType): - _name: str + pass @strawberry_django.type( @@ -598,7 +595,6 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin filters=PowerOutletTemplateFilter ) class PowerOutletTemplateType(ModularComponentTemplateType): - _name: str power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None @@ -630,8 +626,6 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): filters=PowerPortTemplateFilter ) class PowerPortTemplateType(ModularComponentTemplateType): - _name: str - poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]] @@ -650,7 +644,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 @@ -703,7 +696,6 @@ class RearPortType(ModularComponentType, CabledObjectMixin): filters=RearPortTemplateFilter ) class RearPortTemplateType(ModularComponentTemplateType): - _name: str color: str frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] @@ -739,7 +731,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 diff --git a/netbox/dcim/migrations/0197_natural_sort_collation.py b/netbox/dcim/migrations/0197_natural_sort_collation.py new file mode 100644 index 000000000..a77632b37 --- /dev/null +++ b/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", + ), + ] diff --git a/netbox/dcim/migrations/0198_natural_ordering.py b/netbox/dcim/migrations/0198_natural_ordering.py new file mode 100644 index 000000000..83e94a195 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/migrations/0197_rename_mac_address_interface__mac_address_macaddress.py b/netbox/dcim/migrations/0199_macaddress.py similarity index 96% rename from netbox/dcim/migrations/0197_rename_mac_address_interface__mac_address_macaddress.py rename to netbox/dcim/migrations/0199_macaddress.py index bbaa3b59d..5e27fa184 100644 --- a/netbox/dcim/migrations/0197_rename_mac_address_interface__mac_address_macaddress.py +++ b/netbox/dcim/migrations/0199_macaddress.py @@ -40,9 +40,9 @@ def populate_macaddress_objects(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0196_qinq_svlan'), + ('dcim', '0198_natural_ordering'), ('extras', '0122_charfield_null_choices'), - ('virtualization', '0046_rename_mac_address_vminterface__mac_address'), + ('virtualization', '0047_vminterface_rename_mac_address'), ] operations = [ diff --git a/netbox/dcim/migrations/0198_remove_interface__mac_address.py b/netbox/dcim/migrations/0200_interface_remove_mac_address.py similarity index 75% rename from netbox/dcim/migrations/0198_remove_interface__mac_address.py rename to netbox/dcim/migrations/0200_interface_remove_mac_address.py index 88e3510e7..1f6c87e1a 100644 --- a/netbox/dcim/migrations/0198_remove_interface__mac_address.py +++ b/netbox/dcim/migrations/0200_interface_remove_mac_address.py @@ -4,7 +4,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('dcim', '0197_rename_mac_address_interface__mac_address_macaddress'), + ('dcim', '0199_macaddress'), ] operations = [ diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 00555d49e..ddd4d2426 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/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')), ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index c663ee34a..d9dfc3fbd 100644 --- a/netbox/dcim/models/device_components.py +++ b/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'), @@ -1308,7 +1304,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')), ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index b9ca2d4da..dd8771e24 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -25,7 +25,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 @@ -585,13 +585,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, @@ -778,7 +773,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', @@ -1323,7 +1318,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'), @@ -1385,7 +1381,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'), diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index d0c6b18b6..284cfe832 100644 --- a/netbox/dcim/models/power.py +++ b/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'), diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 013dfb619..08b7f5a35 100644 --- a/netbox/dcim/models/racks.py +++ b/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( diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index a290f4119..0985a8d7a 100644 --- a/netbox/dcim/models/sites.py +++ b/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') diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f31caf1c3..530d6c25b 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -143,7 +143,6 @@ class PlatformTable(NetBoxTable): class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): name = tables.TemplateColumn( verbose_name=_('Name'), - order_by=('_name',), template_code=DEVICE_LINK, linkify=True ) @@ -299,7 +298,6 @@ class DeviceComponentTable(NetBoxTable): name = tables.Column( verbose_name=_('Name'), linkify=True, - order_by=('_name',) ) device_status = columns.ChoiceFieldColumn( accessor=tables.A('device__status'), @@ -402,7 +400,6 @@ class DeviceConsolePortTable(ConsolePortTable): name = tables.TemplateColumn( verbose_name=_('Name'), template_code=' {{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -444,7 +441,6 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable): verbose_name=_('Name'), template_code=' ' '{{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -493,7 +489,6 @@ class DevicePowerPortTable(PowerPortTable): verbose_name=_('Name'), template_code=' ' '{{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -542,7 +537,6 @@ class DevicePowerOutletTable(PowerOutletTable): name = tables.TemplateColumn( verbose_name=_('Name'), template_code=' {{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -561,6 +555,11 @@ class DevicePowerOutletTable(PowerOutletTable): class BaseInterfaceTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True, + order_by=('_name',) + ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), ) @@ -608,42 +607,7 @@ class BaseInterfaceTable(NetBoxTable): return ",".join([str(obj) for obj in value.all()]) -class MACAddressTable(NetBoxTable): - mac_address = tables.TemplateColumn( - template_code=MACADDRESS_LINK, - verbose_name=_('MAC Address') - ) - assigned_object = tables.Column( - linkify=True, - orderable=False, - verbose_name=_('Interface') - ) - assigned_object_parent = tables.Column( - accessor='assigned_object__parent_object', - linkify=True, - orderable=False, - verbose_name=_('Parent') - ) - is_primary = columns.BooleanColumn( - verbose_name=_('Primary for Interface'), - false_mark=None - ) - tags = columns.TagColumn( - url_name='dcim:macaddress_list' - ) - actions = columns.ActionsColumn( - extra_buttons=MACADDRESS_COPY_BUTTON - ) - - class Meta(DeviceComponentTable.Meta): - model = models.MACAddress - fields = ( - 'pk', 'id', 'mac_address', 'assigned_object', 'created', 'last_updated', 'is_primary', - ) - default_columns = ('pk', 'mac_address', 'assigned_object', 'is_primary') - - -class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable): +class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpointTable): device = tables.Column( verbose_name=_('Device'), linkify={ @@ -782,7 +746,6 @@ class DeviceFrontPortTable(FrontPortTable): verbose_name=_('Name'), template_code=' ' '{{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -829,7 +792,6 @@ class DeviceRearPortTable(RearPortTable): verbose_name=_('Name'), template_code=' ' '{{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -892,7 +854,6 @@ class DeviceDeviceBayTable(DeviceBayTable): verbose_name=_('Name'), template_code=' {{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = columns.ActionsColumn( @@ -961,7 +922,6 @@ class DeviceModuleBayTable(ModuleBayTable): name = columns.MPTTColumn( verbose_name=_('Name'), linkify=True, - order_by=Accessor('_name') ) actions = columns.ActionsColumn( extra_buttons=MODULEBAY_BUTTONS @@ -1028,7 +988,6 @@ class DeviceInventoryItemTable(InventoryItemTable): verbose_name=_('Name'), template_code='' '{{ value }}', - order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) @@ -1150,3 +1109,38 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable): default_columns = ( 'pk', 'name', 'identifier', 'status', 'tenant', 'primary_ip', ) + + +class MACAddressTable(NetBoxTable): + mac_address = tables.TemplateColumn( + template_code=MACADDRESS_LINK, + verbose_name=_('MAC Address') + ) + assigned_object = tables.Column( + linkify=True, + orderable=False, + verbose_name=_('Interface') + ) + assigned_object_parent = tables.Column( + accessor='assigned_object__parent_object', + linkify=True, + orderable=False, + verbose_name=_('Parent') + ) + is_primary = columns.BooleanColumn( + verbose_name=_('Primary for Interface'), + false_mark=None + ) + tags = columns.TagColumn( + url_name='dcim:macaddress_list' + ) + actions = columns.ActionsColumn( + extra_buttons=MACADDRESS_COPY_BUTTON + ) + + class Meta(DeviceComponentTable.Meta): + model = models.MACAddress + fields = ( + 'pk', 'id', 'mac_address', 'assigned_object', 'created', 'last_updated', 'is_primary', + ) + default_columns = ('pk', 'mac_address', 'assigned_object', 'is_primary') diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index e8a4e35f1..a7f8f08e8 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/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'), ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index a6b704161..dbd99ca24 100644 --- a/netbox/dcim/tables/racks.py +++ b/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( diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index cc2425b68..4bd0ea877 100644 --- a/netbox/dcim/views.py +++ b/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([ diff --git a/netbox/ipam/migrations/0076_natural_ordering.py b/netbox/ipam/migrations/0076_natural_ordering.py new file mode 100644 index 000000000..8c7bfaea1 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/ipam/models/asns.py b/netbox/ipam/models/asns.py index eb47426b2..c1d251301 100644 --- a/netbox/ipam/models/asns.py +++ b/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'), diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 7832cfc67..fa31fd608 100644 --- a/netbox/ipam/models/vlans.py +++ b/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'), diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py index 26afb7927..6a8b8d649 100644 --- a/netbox/ipam/models/vrfs.py +++ b/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', diff --git a/netbox/tenancy/migrations/0017_natural_ordering.py b/netbox/tenancy/migrations/0017_natural_ordering.py new file mode 100644 index 000000000..de1fb49aa --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 24ffef0cf..3969c8317 100644 --- a/netbox/tenancy/models/contacts.py +++ b/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'), diff --git a/netbox/tenancy/models/tenants.py b/netbox/tenancy/models/tenants.py index 7a2d9c2f8..55f0c5933 100644 --- a/netbox/tenancy/models/tenants.py +++ b/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'), diff --git a/netbox/utilities/fields.py b/netbox/utilities/fields.py index ee71223cb..1d16a1d3f 100644 --- a/netbox/utilities/fields.py +++ b/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) diff --git a/netbox/virtualization/api/serializers_/clusters.py b/netbox/virtualization/api/serializers_/clusters.py index c0b636e33..450924fef 100644 --- a/netbox/virtualization/api/serializers_/clusters.py +++ b/netbox/virtualization/api/serializers_/clusters.py @@ -59,6 +59,14 @@ class ClusterSerializer(NetBoxModelSerializer): ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope = serializers.SerializerMethodField(read_only=True) + allocated_vcpus = serializers.DecimalField( + read_only=True, + max_digits=8, + decimal_places=2, + + ) + allocated_memory = serializers.IntegerField(read_only=True) + allocated_disk = serializers.IntegerField(read_only=True) # Related object counts device_count = RelatedObjectCountField('devices') @@ -69,7 +77,7 @@ class ClusterSerializer(NetBoxModelSerializer): fields = [ 'id', 'url', 'display_url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'scope_id', 'scope', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', - 'virtualmachine_count', + 'virtualmachine_count', 'allocated_vcpus', 'allocated_memory', 'allocated_disk' ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index fdf1d71be..93980ce28 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -1,3 +1,4 @@ +from django.db.models import Sum from rest_framework.routers import APIRootView from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin @@ -33,7 +34,11 @@ class ClusterGroupViewSet(NetBoxModelViewSet): class ClusterViewSet(NetBoxModelViewSet): - queryset = Cluster.objects.all() + queryset = Cluster.objects.prefetch_related('virtual_machines').annotate( + allocated_vcpus=Sum('virtual_machines__vcpus'), + allocated_memory=Sum('virtual_machines__memory'), + allocated_disk=Sum('virtual_machines__disk'), + ) serializer_class = serializers.ClusterSerializer filterset_class = filtersets.ClusterFilterSet diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 3085c927c..35e783fa7 100644 --- a/netbox/virtualization/graphql/types.py +++ b/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,8 @@ 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 untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None diff --git a/netbox/virtualization/migrations/0046_natural_ordering.py b/netbox/virtualization/migrations/0046_natural_ordering.py new file mode 100644 index 000000000..9284b6331 --- /dev/null +++ b/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', + ), + ] diff --git a/netbox/virtualization/migrations/0046_rename_mac_address_vminterface__mac_address.py b/netbox/virtualization/migrations/0047_vminterface_rename_mac_address.py similarity index 81% rename from netbox/virtualization/migrations/0046_rename_mac_address_vminterface__mac_address.py rename to netbox/virtualization/migrations/0047_vminterface_rename_mac_address.py index 1efcebd3d..bfbe33d61 100644 --- a/netbox/virtualization/migrations/0046_rename_mac_address_vminterface__mac_address.py +++ b/netbox/virtualization/migrations/0047_vminterface_rename_mac_address.py @@ -4,7 +4,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('virtualization', '0045_clusters_cached_relations'), + ('virtualization', '0046_natural_ordering'), ] operations = [ diff --git a/netbox/virtualization/migrations/0047_remove_vminterface__mac_address.py b/netbox/virtualization/migrations/0048_vminterface_remove__mac_address.py similarity index 75% rename from netbox/virtualization/migrations/0047_remove_vminterface__mac_address.py rename to netbox/virtualization/migrations/0048_vminterface_remove__mac_address.py index c0958ce3a..bfa0e09df 100644 --- a/netbox/virtualization/migrations/0047_remove_vminterface__mac_address.py +++ b/netbox/virtualization/migrations/0048_vminterface_remove__mac_address.py @@ -4,7 +4,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('virtualization', '0046_rename_mac_address_vminterface__mac_address'), + ('virtualization', '0047_vminterface_rename_mac_address'), ] operations = [ diff --git a/netbox/virtualization/models/clusters.py b/netbox/virtualization/models/clusters.py index 601ee7f23..9f7b97e29 100644 --- a/netbox/virtualization/models/clusters.py +++ b/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'), diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 80ef90871..fab30c6f2 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/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', @@ -364,6 +358,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() @@ -422,3 +417,4 @@ class VirtualDisk(ComponentModel, TrackingModelMixin): class Meta(ComponentModel.Meta): verbose_name = _('virtual disk') verbose_name_plural = _('virtual disks') + ordering = ('virtual_machine', 'name') diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index 4a3138711..26d32f8cf 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/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( diff --git a/netbox/vpn/migrations/0007_natural_ordering.py b/netbox/vpn/migrations/0007_natural_ordering.py new file mode 100644 index 000000000..01dd4620f --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/vpn/models/crypto.py b/netbox/vpn/models/crypto.py index 2b721ec29..8e991b578 100644 --- a/netbox/vpn/models/crypto.py +++ b/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'), diff --git a/netbox/vpn/models/l2vpn.py b/netbox/vpn/models/l2vpn.py index b799ab32d..3e562531d 100644 --- a/netbox/vpn/models/l2vpn.py +++ b/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'), diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py index 3a0f1dc14..714024a81 100644 --- a/netbox/vpn/models/tunnels.py +++ b/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'), diff --git a/netbox/wireless/migrations/0012_natural_ordering.py b/netbox/wireless/migrations/0012_natural_ordering.py new file mode 100644 index 000000000..da818bdd9 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index d78c893a6..61ff72bc1 100644 --- a/netbox/wireless/models.py +++ b/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'),