Repository: nautobot/nautobot

Issue:
Main to develop post v1.5.3
Post-release housekeeping.



Discussion:

Resolving patch:
diff --git a/nautobot/core/utilities.py b/nautobot/core/utilities.py
new file mode 100644
--- /dev/null
+++ b/nautobot/core/utilities.py
@@ -0,0 +1,34 @@
+from django.core.exceptions import FieldError
+
+from nautobot.utilities.utils import is_uuid
+
+
+def check_filter_for_display(filters, field_name, values):
+    """
+    Return any additional context data for the template.
+
+    Args:
+        filters (OrderedDict): The output of `.get_filters()` of a desired FilterSet
+        field_name (str): The name of the filter to get a label for and lookup values
+        values (list[str]): List of strings that may be PKs to look up
+
+    Returns:
+        (list): A two item list containing:
+            - [0]: (str) Resolved field name, whether that's a field label of fallback to inputted `field_name` if label unavailable
+            - [1]: (list) Resolved field values, the return of `.display` property of the object, or the original inputted value
+    """
+    values = values if isinstance(values, (list, tuple)) else [values]
+
+    if field_name not in filters.keys():
+        return [field_name, values]
+
+    label = filters[field_name].label if filters[field_name].label else field_name
+    if len(values) == 0 or not hasattr(filters[field_name], "queryset") or not is_uuid(values[0]):
+        return [label, values]
+    else:
+        try:
+            filtered_results = filters[field_name].queryset.filter(pk__in=values)
+            new_values = [result.display if hasattr(result, "display") else str(result) for result in filtered_results]
+            return [label, new_values]
+        except (FieldError, AttributeError):
+            return [label, values]
diff --git a/nautobot/core/views/generic.py b/nautobot/core/views/generic.py
--- a/nautobot/core/views/generic.py
+++ b/nautobot/core/views/generic.py
@@ -23,6 +23,7 @@
 from django_tables2 import RequestConfig
 
 from nautobot.core.forms import SearchForm
+from nautobot.core.utilities import check_filter_for_display
 from nautobot.extras.models import CustomField, ExportTemplate
 from nautobot.extras.models.change_logging import ChangeLoggedModel
 from nautobot.utilities.error_handlers import handle_protectederror
@@ -244,6 +245,7 @@ def get(self, request):
         if self.filterset:
             filter_params = self.get_filter_params(request)
             filterset = self.filterset(filter_params, self.queryset)
+            filterset_filters = filterset.get_filters()
             self.queryset = filterset.qs
             if not filterset.is_valid():
                 messages.error(
@@ -253,7 +255,7 @@ def get(self, request):
                 self.queryset = self.queryset.none()
 
             display_filter_params = [
-                [field_name, values if isinstance(values, (list, tuple)) else [values]]
+                check_filter_for_display(filterset_filters, field_name, values)
                 for field_name, values in filter_params.items()
             ]
 
diff --git a/nautobot/core/views/renderers.py b/nautobot/core/views/renderers.py
--- a/nautobot/core/views/renderers.py
+++ b/nautobot/core/views/renderers.py
@@ -8,6 +8,7 @@
 from rest_framework import renderers
 
 from nautobot.core.forms import SearchForm
+from nautobot.core.utilities import check_filter_for_display
 from nautobot.extras.models.change_logging import ChangeLoggedModel, ObjectChange
 from nautobot.extras.utils import get_base_template
 from nautobot.utilities.forms import (
@@ -165,6 +166,7 @@ def get_context(self, data, accepted_media_type, renderer_context):
                 filter_params = self.get_filter_params(view, request)
                 if view.filterset_class is not None:
                     filterset = view.filterset_class(filter_params, view.queryset)
+                    filterset_filters = filterset.get_filters()
                     view.queryset = filterset.qs
                     if not filterset.is_valid():
                         messages.error(
@@ -174,7 +176,7 @@ def get_context(self, data, accepted_media_type, renderer_context):
                         view.queryset = view.queryset.none()
 
                     display_filter_params = [
-                        [field_name, values if isinstance(values, (list, tuple)) else [values]]
+                        check_filter_for_display(filterset_filters, field_name, values)
                         for field_name, values in filter_params.items()
                     ]
                     if view.filterset_form_class is not None:


Test patch:
diff --git a/nautobot/core/tests/test_utilities.py b/nautobot/core/tests/test_utilities.py
new file mode 100644
--- /dev/null
+++ b/nautobot/core/tests/test_utilities.py
@@ -0,0 +1,74 @@
+from django.test import TestCase
+
+from nautobot.core.utilities import check_filter_for_display
+
+from nautobot.dcim.filters import DeviceFilterSet
+from nautobot.dcim.models import DeviceType, DeviceRedundancyGroup
+
+
+class CheckFilterForDisplayTest(TestCase):
+    """
+    Validate the operation of check_filter_for_display().
+    """
+
+    def test_check_filter_for_display(self):
+
+        device_filter_set_filters = DeviceFilterSet().get_filters()
+
+        with self.subTest("Test invalid filter case (field_name not found)"):
+            expected_output = ["fake_field_name", ["example_field_value"]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "fake_field_name", ["example_field_value"]),
+                expected_output,
+            )
+
+        with self.subTest("Test values are converted to list"):
+            expected_output = ["fake_field_name", ["example_field_value"]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "fake_field_name", "example_field_value"),
+                expected_output,
+            )
+
+        with self.subTest("Test get field label, none exists (fallback)"):
+            expected_output = ["id", ["example_field_value"]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "id", ["example_field_value"]), expected_output
+            )
+
+        with self.subTest("Test get field label, exists"):
+            expected_output = ["Has interfaces", ["example_field_value"]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "has_interfaces", ["example_field_value"]),
+                expected_output,
+            )
+
+        with self.subTest(
+            "Test get value display, falls back to string representation (also NaturalKeyOrPKMultipleChoiceFilter)"
+        ):
+            example_obj = DeviceRedundancyGroup.objects.first()
+            expected_output = ["Device Redundancy Groups (slug or ID)", [str(example_obj)]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "device_redundancy_group", [str(example_obj.pk)]),
+                expected_output,
+            )
+
+        with self.subTest("Test get value display (also legacy filter ModelMultipleChoiceFilter)"):
+            example_obj = DeviceType.objects.first()
+            expected_output = ["Device type (ID)", [example_obj.display]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "device_type_id", [str(example_obj.pk)]),
+                expected_output,
+            )
+
+        with self.subTest("Test skip non-UUID value display (legacy, ex: ModelMultipleChoiceFilter)"):
+            expected_output = ["Manufacturer (slug)", ["fake_slug"]]
+
+            self.assertEqual(
+                check_filter_for_display(device_filter_set_filters, "manufacturer", ["fake_slug"]), expected_output
+            )