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>
This commit is contained in:
Arthur Hanson 2024-11-15 06:32:09 -08:00 committed by GitHub
parent 75aeaab8ee
commit 6ab0792f02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 622 additions and 150 deletions

View File

@ -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),
),
]

View File

@ -21,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True, unique=True,
help_text=_('Full name of the provider') help_text=_('Full name of the provider'),
db_collation="natural_sort"
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'), verbose_name=_('slug'),
@ -95,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100 max_length=100,
db_collation="natural_sort"
) )
provider = models.ForeignKey( provider = models.ForeignKey(
to='circuits.Provider', to='circuits.Provider',

View File

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

View File

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

View File

@ -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",
),
]

View File

@ -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),
),
]

View File

@ -44,12 +44,8 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
max_length=64, max_length=64,
help_text=_( help_text=_(
"{module} is accepted as a substitution for the module bay position when attached to a module type." "{module} is accepted as a substitution for the module bay position when attached to a module type."
) ),
) db_collation="natural_sort"
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
) )
label = models.CharField( label = models.CharField(
verbose_name=_('label'), verbose_name=_('label'),
@ -65,7 +61,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
class Meta: class Meta:
abstract = True abstract = True
ordering = ('device_type', '_name') ordering = ('device_type', 'name')
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('device_type', 'name'), fields=('device_type', 'name'),
@ -125,7 +121,7 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
class Meta: class Meta:
abstract = True abstract = True
ordering = ('device_type', 'module_type', '_name') ordering = ('device_type', 'module_type', 'name')
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('device_type', 'name'), fields=('device_type', 'name'),
@ -782,7 +778,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
component_model = InventoryItem component_model = InventoryItem
class Meta: class Meta:
ordering = ('device_type__id', 'parent__id', '_name') ordering = ('device_type__id', 'parent__id', 'name')
indexes = ( indexes = (
models.Index(fields=('component_type', 'component_id')), models.Index(fields=('component_type', 'component_id')),
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
),
]

View File

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

View File

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

View File

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

View File

@ -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),
),
]

View File

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

View File

@ -18,7 +18,8 @@ class TenantGroup(NestedGroupModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True,
db_collation="natural_sort"
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'), verbose_name=_('slug'),
@ -39,7 +40,8 @@ class Tenant(ContactsMixin, PrimaryModel):
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100 max_length=100,
db_collation="natural_sort"
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'), verbose_name=_('slug'),

View File

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

View File

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

View File

@ -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',
),
]

View File

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

View File

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

View File

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

View File

@ -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),
),
]

View File

@ -22,7 +22,8 @@ class IKEProposal(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True,
db_collation="natural_sort"
) )
authentication_method = models.CharField( authentication_method = models.CharField(
verbose_name=('authentication method'), verbose_name=('authentication method'),
@ -67,7 +68,8 @@ class IKEPolicy(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True,
db_collation="natural_sort"
) )
version = models.PositiveSmallIntegerField( version = models.PositiveSmallIntegerField(
verbose_name=_('version'), verbose_name=_('version'),
@ -125,7 +127,8 @@ class IPSecProposal(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True,
db_collation="natural_sort"
) )
encryption_algorithm = models.CharField( encryption_algorithm = models.CharField(
verbose_name=_('encryption'), verbose_name=_('encryption'),
@ -176,7 +179,8 @@ class IPSecPolicy(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True,
db_collation="natural_sort"
) )
proposals = models.ManyToManyField( proposals = models.ManyToManyField(
to='vpn.IPSecProposal', to='vpn.IPSecProposal',
@ -211,7 +215,8 @@ class IPSecProfile(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True,
db_collation="natural_sort"
) )
mode = models.CharField( mode = models.CharField(
verbose_name=_('mode'), verbose_name=_('mode'),

View File

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

View File

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

View File

@ -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),
),
]

View File

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