Repository: netbox-community/netbox

Issue:
Natural ordering breaks when sorting objects by name
### Issue type
[ ] Feature request <!-- An enhancement of existing functionality -->
[x] Bug report      <!-- Unexpected or erroneous behavior -->
[ ] Documentation   <!-- A modification to the documentation -->

### Environment
* Python version:  3.5.2
* NetBox version:  2.3.4

### Description
Natural ordering is broken in object tables. For example:

1. Create three new sites named `Foo1`, `Foo2`, and `Foo10`.
2. Navigate to the sites list. The three sites will be listed in order.
3. Click the "Name" header to order sites by name.
4. The sites are now ordered `Foo1`, `Foo10`, `Foo2`.

This occurs because the default natural ordering is being overridden by the specification of an explicit table field (e.g. `name`).


Discussion:

Resolving patch:
diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -77,9 +77,7 @@ def to_csv(self):
 #
 
 class SiteManager(NaturalOrderByManager):
-
-    def get_queryset(self):
-        return self.natural_order_by('name')
+    natural_order_field = 'name'
 
 
 @python_2_unicode_compatible
@@ -308,9 +306,7 @@ def to_csv(self):
 
 
 class RackManager(NaturalOrderByManager):
-
-    def get_queryset(self):
-        return self.natural_order_by('site__name', 'name')
+    natural_order_field = 'name'
 
 
 @python_2_unicode_compatible
@@ -1098,9 +1094,7 @@ def to_csv(self):
 
 
 class DeviceManager(NaturalOrderByManager):
-
-    def get_queryset(self):
-        return self.natural_order_by('name')
+    natural_order_field = 'name'
 
 
 @python_2_unicode_compatible
diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -175,7 +175,7 @@ class Meta(BaseTable.Meta):
 
 class SiteTable(BaseTable):
     pk = ToggleColumn()
-    name = tables.LinkColumn()
+    name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
     tenant = tables.TemplateColumn(template_code=COL_TENANT)
@@ -236,7 +236,7 @@ class Meta(BaseTable.Meta):
 
 class RackTable(BaseTable):
     pk = ToggleColumn()
-    name = tables.LinkColumn()
+    name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     tenant = tables.TemplateColumn(template_code=COL_TENANT)
@@ -469,7 +469,10 @@ class Meta(BaseTable.Meta):
 
 class DeviceTable(BaseTable):
     pk = ToggleColumn()
-    name = tables.TemplateColumn(template_code=DEVICE_LINK)
+    name = tables.TemplateColumn(
+        order_by=('_nat1', '_nat2', '_nat3'),
+        template_code=DEVICE_LINK
+    )
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     tenant = tables.TemplateColumn(template_code=COL_TENANT)
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
diff --git a/netbox/utilities/managers.py b/netbox/utilities/managers.py
--- a/netbox/utilities/managers.py
+++ b/netbox/utilities/managers.py
@@ -4,29 +4,35 @@
 
 
 class NaturalOrderByManager(Manager):
+    """
+    Order objects naturally by a designated field. Leading and/or trailing digits of values within this field will be
+    cast as independent integers and sorted accordingly. For example, "Foo2" will be ordered before "Foo10", even though
+    the digit 1 is normally ordered before the digit 2.
+    """
+    natural_order_field = None
 
-    def natural_order_by(self, *fields):
-        """
-        Attempt to order records naturally by segmenting a field into three parts:
+    def get_queryset(self):
 
-        1. Leading integer (if any)
-        2. Middle portion
-        3. Trailing integer (if any)
+        queryset = super(NaturalOrderByManager, self).get_queryset()
 
-        :param fields: The fields on which to order the queryset. The last field in the list will be ordered naturally.
-        """
         db_table = self.model._meta.db_table
-        primary_field = fields[-1]
+        db_field = self.natural_order_field
 
-        id1 = '_{}_{}1'.format(db_table, primary_field)
-        id2 = '_{}_{}2'.format(db_table, primary_field)
-        id3 = '_{}_{}3'.format(db_table, primary_field)
-
-        queryset = super(NaturalOrderByManager, self).get_queryset().extra(select={
-            id1: "CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)".format(db_table, primary_field),
-            id2: "SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, primary_field),
-            id3: "CAST(SUBSTRING({}.{} FROM '(\d{{1,9}})$') AS integer)".format(db_table, primary_field),
+        # Append the three subfields derived from the designated natural ordering field
+        queryset = queryset.extra(select={
+            '_nat1': "CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)".format(db_table, db_field),
+            '_nat2': "SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, db_field),
+            '_nat3': "CAST(SUBSTRING({}.{} FROM '(\d{{1,9}})$') AS integer)".format(db_table, db_field),
         })
-        ordering = fields[0:-1] + (id1, id2, id3)
+
+        # Replace any instance of the designated natural ordering field with its three subfields
+        ordering = []
+        for field in self.model._meta.ordering:
+            if field == self.natural_order_field:
+                ordering.append('_nat1')
+                ordering.append('_nat2')
+                ordering.append('_nat3')
+            else:
+                ordering.append(field)
 
         return queryset.order_by(*ordering)


Test patch: