diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index b0f5754b2..e9c5f0f78 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -10,6 +10,7 @@ * [#2994](https://github.com/netbox-community/netbox/issues/2994) - Prevent modifying termination points of existing cable to ensure end-to-end path integrity * [#3356](https://github.com/netbox-community/netbox/issues/3356) - Correct Swagger schema specification for the available prefixes/IPs API endpoints +* [#4336](https://github.com/netbox-community/netbox/issues/4336) - Ensure interfaces without a subinterface ID are ordered before subinterface zero * [#4361](https://github.com/netbox-community/netbox/issues/4361) - Fix Type of `connection_state` in Swagger schema * [#4388](https://github.com/netbox-community/netbox/issues/4388) - Fix detection of connected endpoints when connecting rear ports * [#4489](https://github.com/netbox-community/netbox/issues/4489) - Fix display of parent/child role on device type view diff --git a/netbox/dcim/migrations/0105_interface_name_collation.py b/netbox/dcim/migrations/0105_interface_name_collation.py new file mode 100644 index 000000000..3079cf5cd --- /dev/null +++ b/netbox/dcim/migrations/0105_interface_name_collation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-21 20:13 + +from django.db import migrations +import utilities.query_functions + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0104_correct_infiniband_types'), + ] + + operations = [ + migrations.AlterModelOptions( + name='interface', + options={'ordering': ('device', utilities.query_functions.CollateAsChar('_name'))}, + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f3cf0e3c8..8c79d89d8 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -16,6 +16,7 @@ from extras.models import ObjectChange, TaggedItem from extras.utils import extras_features from utilities.fields import NaturalOrderingField from utilities.ordering import naturalize_interface +from utilities.query_functions import CollateAsChar from utilities.utils import serialize_object from virtualization.choices import VMInterfaceTypeChoices @@ -676,7 +677,7 @@ class Interface(CableTermination, ComponentModel): class Meta: # TODO: ordering and unique_together should include virtual_machine - ordering = ('device', '_name') + ordering = ('device', CollateAsChar('_name')) unique_together = ('device', 'name') def __str__(self): diff --git a/netbox/dcim/tests/test_natural_ordering.py b/netbox/dcim/tests/test_natural_ordering.py index 2d2b5c4dc..5c42b3ab4 100644 --- a/netbox/dcim/tests/test_natural_ordering.py +++ b/netbox/dcim/tests/test_natural_ordering.py @@ -23,28 +23,34 @@ class NaturalOrderingTestCase(TestCase): INTERFACES = [ '0', + '0.0', '0.1', '0.2', '0.10', '0.100', '0:1', + '0:1.0', '0:1.1', '0:1.2', '0:1.10', '0:2', + '0:2.0', '0:2.1', '0:2.2', '0:2.10', '1', + '1.0', '1.1', '1.2', '1.10', '1.100', '1:1', + '1:1.0', '1:1.1', '1:1.2', '1:1.10', '1:2', + '1:2.0', '1:2.1', '1:2.2', '1:2.10', diff --git a/netbox/utilities/ordering.py b/netbox/utilities/ordering.py index 346a99488..c5287b1e1 100644 --- a/netbox/utilities/ordering.py +++ b/netbox/utilities/ordering.py @@ -75,7 +75,7 @@ def naturalize_interface(value, max_length): if part is not None: output += part.rjust(6, '0') else: - output += '000000' + output += '......' # Finally, naturalize any remaining text and append it if match.group('remainder') is not None and len(output) < max_length: diff --git a/netbox/utilities/query_functions.py b/netbox/utilities/query_functions.py new file mode 100644 index 000000000..ee4310ea7 --- /dev/null +++ b/netbox/utilities/query_functions.py @@ -0,0 +1,9 @@ +from django.db.models import F, Func + + +class CollateAsChar(Func): + """ + Disregard localization by collating a field as a plain character string. Helpful for ensuring predictable ordering. + """ + function = 'C' + template = '(%(expressions)s) COLLATE "%(function)s"' diff --git a/netbox/utilities/tests/test_ordering.py b/netbox/utilities/tests/test_ordering.py index d535443ea..8e85f9e8c 100644 --- a/netbox/utilities/tests/test_ordering.py +++ b/netbox/utilities/tests/test_ordering.py @@ -30,29 +30,32 @@ class NaturalizationTestCase(TestCase): # Original, naturalized data = ( + # IOS/JunOS-style - ('Gi', '9999999999999999Gi000000000000000000'), - ('Gi1', '9999999999999999Gi000001000000000000'), - ('Gi1.0', '9999999999999999Gi000001000000000000'), - ('Gi1.1', '9999999999999999Gi000001000000000001'), - ('Gi1:0', '9999999999999999Gi000001000000000000'), + ('Gi', '9999999999999999Gi..................'), + ('Gi1', '9999999999999999Gi000001............'), + ('Gi1.0', '9999999999999999Gi000001......000000'), + ('Gi1.1', '9999999999999999Gi000001......000001'), + ('Gi1:0', '9999999999999999Gi000001000000......'), ('Gi1:0.0', '9999999999999999Gi000001000000000000'), ('Gi1:0.1', '9999999999999999Gi000001000000000001'), - ('Gi1:1', '9999999999999999Gi000001000001000000'), + ('Gi1:1', '9999999999999999Gi000001000001......'), ('Gi1:1.0', '9999999999999999Gi000001000001000000'), ('Gi1:1.1', '9999999999999999Gi000001000001000001'), - ('Gi1/2', '0001999999999999Gi000002000000000000'), - ('Gi1/2/3', '0001000299999999Gi000003000000000000'), - ('Gi1/2/3/4', '0001000200039999Gi000004000000000000'), - ('Gi1/2/3/4/5', '0001000200030004Gi000005000000000000'), - ('Gi1/2/3/4/5:6', '0001000200030004Gi000005000006000000'), + ('Gi1/2', '0001999999999999Gi000002............'), + ('Gi1/2/3', '0001000299999999Gi000003............'), + ('Gi1/2/3/4', '0001000200039999Gi000004............'), + ('Gi1/2/3/4/5', '0001000200030004Gi000005............'), + ('Gi1/2/3/4/5:6', '0001000200030004Gi000005000006......'), ('Gi1/2/3/4/5:6.7', '0001000200030004Gi000005000006000007'), + # Generic - ('Interface 1', '9999999999999999Interface 000001000000000000'), - ('Interface 1 (other)', '9999999999999999Interface 000001000000000000 (other)'), - ('Interface 99', '9999999999999999Interface 000099000000000000'), - ('PCIe1-p1', '9999999999999999PCIe000001000000000000-p00000001'), - ('PCIe1-p99', '9999999999999999PCIe000001000000000000-p00000099'), + ('Interface 1', '9999999999999999Interface 000001............'), + ('Interface 1 (other)', '9999999999999999Interface 000001............ (other)'), + ('Interface 99', '9999999999999999Interface 000099............'), + ('PCIe1-p1', '9999999999999999PCIe000001............-p00000001'), + ('PCIe1-p99', '9999999999999999PCIe000001............-p00000099'), + ) for origin, naturalized in data: