Repository: netbox-community/netbox
Pull request: #9665 Rack Elevation Sorting Enhancements
<!--
    Thank you for your interest in contributing to NetBox! Please note that
    our contribution policy requires that a feature request or bug report be
    approved and assigned prior to filing a pull request. This helps avoid
    wasting time and effort on something that we might not be able to accept.

    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
    TO YOU, IT WE BE CLOSED AUTOMATICALLY.

    Specify your assigned issue number on the line below.
-->
### Fixes: #9665

<!--
    Please include a summary of the proposed changes below.
-->
On Rack Elevation list add a "Sort By" dropdown in-place of the buttons, with options "Name (A-Z)", "Name (Z-A)", "Facility ID (A-Z)", "Facility ID (Z-A)"
![rack sort](https://user-images.githubusercontent.com/99642/187790796-f8786a02-eca7-4554-91ab-503cbfa95886.png)


Problem statement:
Rack Elevation Sorting Enhancement
### NetBox version

v3.2.2

### Feature type

Change to existing functionality

### Proposed functionality

Would be a helpful feature if we were able to sort the rack elevation by rack facility ID.

Currently, the rack elevation view sorts only on rack name. It can be sorted in "normal" (numerically ascending) or "reversed" (numerically descending.)"

### Use case

Rack naming conventions are dependent on organization, not all entities name or number racks in a contiguous linear fashion from left to right. In our case, our center racks are numbered racks one and two and numbering increases as you move further from the center racks. It would be a helpful feature to see the rack elevation ordered by facility ID from left to right while retaining a custom rack naming at the top.

### Database changes

Likely none, but unknown.

### External dependencies

None

Hints from issue discussion:
@jeremystretch The current sort is a button-toggle, which wouldn't work for adding any new sorting.  If we add this I see two potential solutions:

1. Add a "Sort By" dropdown in-place of the buttons, with options "Name (A-Z)", "Name (Z-A)", "Facility ID (A-Z)", "Facility ID (Z-A)" could also use (Ascending) / (Descending) instead of (A-Z).  
2. Move the button below the tabs and have a "Sort By: " and then the dropdown showing the current sorting. (see screenshot)

I looked around and all the other sorting is done by column headers, so this looks like a sort of one-off UI.

![sortby](https://user-images.githubusercontent.com/99642/186536188-47d35b2e-9d90-4bd4-b051-aa99741f02f1.png)

I think a dropdown with all four combinations makes sense. A-Z and Z-A is fine.

Commit message: #9665 Rack Elevation Sorting Enhancements

Patch to review:
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 39b2340e1c9..0f6e388caf7 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -590,9 +590,16 @@ def get(self, request):
         total_count = racks.count()
 
         # Determine ordering
-        reverse = bool(request.GET.get('reverse', False))
-        if reverse:
-            racks = racks.reverse()
+        sort = request.GET.get('sort', "name")
+        if sort:
+            if sort == 'name':
+                racks = racks.order_by('name')
+            elif sort == '-name':
+                racks = racks.order_by('-name')
+            elif sort == 'facility':
+                racks = racks.order_by('facility_id')
+            elif sort == '-facility':
+                racks = racks.order_by('-facility_id')
 
         # Pagination
         per_page = get_paginate_count(request)
@@ -614,7 +621,7 @@ def get(self, request):
             'paginator': paginator,
             'page': page,
             'total_count': total_count,
-            'reverse': reverse,
+            'sort': sort,
             'rack_face': rack_face,
             'filter_form': forms.RackElevationFilterForm(request.GET),
         })
diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html
index 87a04790082..13953b4b121 100644
--- a/netbox/templates/dcim/rack_elevation_list.html
+++ b/netbox/templates/dcim/rack_elevation_list.html
@@ -18,9 +18,16 @@
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
             </div>
-            <div class="btn-group btn-group-sm" role="group">
-                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
-                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
+            <div class="dropdown">
+              <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                <i class="mdi mdi-sort"></i>&nbsp;Sort By ({{ sort }})
+              </button>
+              <ul class="dropdown-menu dropdown-menu-end">
+                <li><a class="dropdown-item{% if sort == 'name' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort='name' %}">Name (A-Z)</a></li>
+                <li><a class="dropdown-item{% if sort == '-name' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request  sort='-name' %}">Name (Z-A)</a></li>
+                <li><a class="dropdown-item{% if sort == 'facility' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort='facility' %}">Facility ID (A-Z)</a></li>
+                <li><a class="dropdown-item{% if sort == '-facility' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort='-facility' %}">Facility ID (Z-A)</a></li>
+              </ul>
             </div>
         </div>
     </div>


Code review comments:
diff_hunk: @@ -590,9 +590,16 @@ def get(self, request):
         total_count = racks.count()
 
         # Determine ordering
-        reverse = bool(request.GET.get('reverse', False))
-        if reverse:
-            racks = racks.reverse()
+        sort = request.GET.get('sort', "name")
+        if sort:
+            if sort == 'name':
+                racks = racks.order_by('name')
+            elif sort == '-name':
+                racks = racks.order_by('-name')
+            elif sort == 'facility':
+                racks = racks.order_by('facility_id')
+            elif sort == '-facility':
+                racks = racks.order_by('-facility_id')
path: netbox/dcim/views.py
text: @user1:
We can replace the switch-like block here with a dictionary mapping. This can be used both for validation and for displaying a human-friendly label for the selection.

```suggestion
        ORDERING_CHOICES = {
            'name': 'Name (A-Z)',
            '-name': 'Name (Z-A)',
            'facility_id': 'Facility ID (A-Z)',
            '-facility_id': 'Facility ID (Z-A)',
        }

        sort = request.GET.get('sort')
        if sort not in ORDERING_CHOICES:
            sort = 'name'
```

diff_hunk: @@ -18,9 +18,16 @@
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
             </div>
-            <div class="btn-group btn-group-sm" role="group">
-                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
-                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
+            <div class="dropdown">
+              <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                <i class="mdi mdi-sort"></i>&nbsp;Sort By ({{ sort }})
+              </button>
+              <ul class="dropdown-menu dropdown-menu-end">
+                <li><a class="dropdown-item{% if sort == 'name' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort='name' %}">Name (A-Z)</a></li>
+                <li><a class="dropdown-item{% if sort == '-name' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request  sort='-name' %}">Name (Z-A)</a></li>
+                <li><a class="dropdown-item{% if sort == 'facility' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort='facility' %}">Facility ID (A-Z)</a></li>
+                <li><a class="dropdown-item{% if sort == '-facility' %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort='-facility' %}">Facility ID (Z-A)</a></li>
path: netbox/templates/dcim/rack_elevation_list.html
text: @user1:
This can be converted to a `for` loop iterating over `sort_choices`.

diff_hunk: @@ -18,9 +18,16 @@
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
             </div>
-            <div class="btn-group btn-group-sm" role="group">
-                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
-                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
+            <div class="dropdown">
+              <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                <i class="mdi mdi-sort"></i>&nbsp;Sort By ({{ sort }})
path: netbox/templates/dcim/rack_elevation_list.html
text: @user1:
We want to avoid exposing the raw sort value to the user, because it may not make sense (e.g. `-facility_id`). What we can do under the view is define a mapping of raw values to human-friendly labels.