Browse Source

Fix order_naturally with unbalanced names and use RawSQL instead of extra

Joey Wilhelm 7 years ago
parent
commit
126a5e5e4e
2 changed files with 98 additions and 16 deletions
  1. 32 16
      netbox/dcim/models.py
  2. 66 0
      netbox/dcim/tests/test_models.py

+ 32 - 16
netbox/dcim/models.py

@@ -13,6 +13,7 @@ from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.db.models import Count, Q, ObjectDoesNotExist
 from django.db.models import Count, Q, ObjectDoesNotExist
+from django.db.models.expressions import RawSQL
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
 
 
@@ -642,15 +643,16 @@ class InterfaceQuerySet(models.QuerySet):
         To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
         To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
         slot, subslot, position, channel, and virtual circuit:
         slot, subslot, position, channel, and virtual circuit:
 
 
-            {type}{slot}/{subslot}/{position}:{channel}.{vc}
+            {type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
 
 
-        Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
+        Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
-        be parsed as follows:
+        would be parsed as follows:
 
 
             name = 'GigabitEthernet'
             name = 'GigabitEthernet'
-            slot =  None
+            slot =  1
-            subslot = 0
+            subslot = 2
-            position = 1
+            position = 3
+            subposition = 0
             channel = None
             channel = None
             vc = 0
             vc = 0
 
 
@@ -659,17 +661,31 @@ class InterfaceQuerySet(models.QuerySet):
         """
         """
         sql_col = '{}.name'.format(self.model._meta.db_table)
         sql_col = '{}.name'.format(self.model._meta.db_table)
         ordering = {
         ordering = {
-            IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
+            IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', 'name'),
-            IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
+            IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', 'name'),
         }[method]
         }[method]
-        return self.extra(select={
+
-            '_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
+        fields = {
-            '_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
+            '_type': RawSQL(r"SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), []),
-            '_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
+            '_slot': RawSQL(r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)') AS integer)".format(sql_col), []),
-            '_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
+            '_subslot': RawSQL(
-            '_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
+                r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+)\/([0-9]+)') AS integer), 0)".format(
-            '_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
+                    sql_col), []
-        }).order_by(*ordering)
+            ),
+            '_position': RawSQL(
+                r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)".format(
+                    sql_col), []
+            ),
+            '_subposition': RawSQL(
+                r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)".format(
+                    sql_col), []
+            ),
+            '_channel': RawSQL(
+                r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col), []),
+            '_vc': RawSQL(r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col), []),
+        }
+
+        return self.annotate(**fields).order_by(*ordering)
 
 
     def connectable(self):
     def connectable(self):
         """
         """

+ 66 - 0
netbox/dcim/tests/test_models.py

@@ -98,3 +98,69 @@ class RackTestCase(TestCase):
             face=None,
             face=None,
         )
         )
         self.assertTrue(pdu)
         self.assertTrue(pdu)
+
+
+class InterfaceTestCase(TestCase):
+
+    def setUp(self):
+
+        self.site = Site.objects.create(
+            name='TestSite1',
+            slug='my-test-site'
+        )
+        self.rack = Rack.objects.create(
+            name='TestRack1',
+            facility_id='A101',
+            site=self.site,
+            u_height=42
+        )
+        self.manufacturer = Manufacturer.objects.create(
+            name='Acme',
+            slug='acme'
+        )
+
+        self.device_type = DeviceType.objects.create(
+            manufacturer=self.manufacturer,
+            model='FrameForwarder 2048',
+            slug='ff2048'
+        )
+        self.role = DeviceRole.objects.create(
+            name='Switch',
+            slug='switch',
+        )
+
+    def test_device_port_order_natural(self):
+        device1 = Device.objects.create(
+            name='TestSwitch1',
+            device_type=self.device_type,
+            device_role=self.role,
+            site=self.site,
+            rack=self.rack,
+            position=10,
+            face=RACK_FACE_REAR,
+        )
+        interface1 = Interface.objects.create(
+            device=device1,
+            name='Ethernet1/3/1'
+        )
+        interface2 = Interface.objects.create(
+            device=device1,
+            name='Ethernet1/5/1'
+        )
+        interface3 = Interface.objects.create(
+            device=device1,
+            name='Ethernet1/4'
+        )
+        interface4 = Interface.objects.create(
+            device=device1,
+            name='Ethernet1/3/2/4'
+        )
+        interface5 = Interface.objects.create(
+            device=device1,
+            name='Ethernet1/3/2/1'
+        )
+
+        self.assertEqual(
+            list(Interface.objects.all().order_naturally()),
+            [interface1, interface5, interface4, interface3, interface2]
+        )