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

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['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 = (
{

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

View File

@ -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(

View File

@ -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([

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(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),

View File

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

View File

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

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(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
title = models.CharField(
verbose_name=_('title'),

View File

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

View File

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

View File

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

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(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
type = models.ForeignKey(
verbose_name=_('type'),

View File

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

View File

@ -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(

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(
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'),

View File

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

View File

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

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(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),