Repository: nautobot/nautobot

Issue:
DeviceFilterSet attributes being stripped of options

### Environment
* Nautobot version (Docker tag too if applicable): 2.0-rc2
* Python version: 3.11
* Database platform, version: latest
* Middleware(s): N/A

I attempted to look on 1.5 or 1.6 but the demo instances weren't immediately loading to see if this is 1.x or not. 

### Steps to Reproduce
1. Go to https://next.demo.nautobot.com/api/docs/#/dcim/dcim_devices_list
2. Observe has_device_bays, created, last_updated, etc. 

<!-- What did you expect to happen? -->
### Expected Behavior

The attributes to match up to their type, such as DateTime, Boolean, Integer, etc.. as shown in the device-types api


<img width="643" alt="image" src="https://github.com/nautobot/nautobot/assets/9260483/315068ea-fb6e-490e-846b-b258a630e628">


<!-- What happened instead? -->
### Observed Behavior

Everything is defaulting to char type.

<img width="698" alt="image" src="https://github.com/nautobot/nautobot/assets/9260483/63143b5e-c896-4e92-8dbe-291e4e34fe77">



Discussion:
The same seems to be true for virtual machines.
demo.nautobot.com on 1.6.1 seems to be working, except for face

<img width="818" alt="image" src="https://github.com/nautobot/nautobot/assets/9260483/b90c3646-5b6c-4d7f-b16a-04a423cbfde6">

sales instance on 1.5.24 seems to only have face choice working?

<img width="711" alt="image" src="https://github.com/nautobot/nautobot/assets/9260483/51c370e2-b0d9-4090-ab04-e0e9b064abe0">




Can confirm that the OpenAPI schema differs between Device and DeviceType in `next`, e.g.:

```yaml
      - name: has_console_ports
        required: false
        in: query
        description: Has console ports
        schema:
          type: string
```

versus:

```yaml
      - in: query
        name: has_console_port_templates
        schema:
          type: boolean
        description: Has console port templates
```
I wonder if this somehow has anything to do with the FilterForm definitions - `DeviceFilterForm` has `has_console_ports` included as a `NullBooleanField`, while `DeviceTypeFilterForm` does not include `has_console_port_templates`.
@glennmatthews my gut says no, since it is stripping off many other things, I realized this when implementing #4378, that the `isnull` was also being stripped out. Look at all of the attributes, not just the `has_*`, I think something else is going on. 
Agreed - the weirdness appears specific to the `dcim/devices/` and `virtualization/virtual-machines/` endpoints; all others appear to be working normally on a quick search of the schema.
Possibly related to some of the config-context handling changes in 2.0, such as #3814?
@glennmatthews also consider, that I see the issue on the one instance of 1.5.24, but not on 1.6.1, though 1.6.1 has an issue with `face_*` choices... So oddness all around. It is not clear if it is something specfic with the deployments (such as version of drf-spectacular) being used, or something else. 

Resolving patch:
diff --git a/nautobot/extras/api/views.py b/nautobot/extras/api/views.py
--- a/nautobot/extras/api/views.py
+++ b/nautobot/extras/api/views.py
@@ -12,7 +12,6 @@
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, ValidationError
-from rest_framework.filters import OrderingFilter
 from rest_framework.parsers import JSONParser, MultiPartParser
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
@@ -20,7 +19,6 @@
 from rest_framework import mixins, viewsets
 
 from nautobot.core.api.authentication import TokenPermissions
-from nautobot.core.api.filter_backends import NautobotFilterBackend
 from nautobot.core.api.utils import get_serializer_for_model
 from nautobot.core.api.views import (
     BulkDestroyModelMixin,
@@ -131,22 +129,6 @@ class ComputedFieldViewSet(ModelViewSet, NotesViewSetMixin):
 #
 
 
-class ConfigContextFilterBackend(NautobotFilterBackend):
-    """
-    Used by views that work with config context models (device and virtual machine).
-
-    Recognizes that "exclude" is not a filterset parameter but rather a view parameter (see ConfigContextQuerySetMixin)
-    """
-
-    def get_filterset_kwargs(self, request, queryset, view):
-        kwargs = super().get_filterset_kwargs(request, queryset, view)
-        try:
-            kwargs["data"].pop("exclude")
-        except KeyError:
-            pass
-        return kwargs
-
-
 class ConfigContextQuerySetMixin:
     """
     Used by views that work with config context models (device and virtual machine).
@@ -154,22 +136,19 @@ class ConfigContextQuerySetMixin:
     data annotation or not.
     """
 
-    filter_backends = [ConfigContextFilterBackend, OrderingFilter]
-
     def get_queryset(self):
         """
         Build the proper queryset based on the request context
 
-        If the `exclude` query param
-        includes `config_context` as a value, return the base queryset.
+        If the `include` query param includes `config_context`, return the queryset annotated with config context.
 
-        Else, return the queryset annotated with config context data
+        Else, return the base queryset.
         """
         queryset = super().get_queryset()
         request = self.get_serializer_context()["request"]
-        if request is not None and "config_context" in request.query_params.get("exclude", []):
-            return queryset
-        return queryset.annotate_config_context_data()
+        if request is not None and "config_context" in request.query_params.get("include", []):
+            return queryset.annotate_config_context_data()
+        return queryset
 
 
 class ConfigContextViewSet(ModelViewSet, NotesViewSetMixin):


Test patch:
diff --git a/nautobot/core/tests/test_openapi.py b/nautobot/core/tests/test_openapi.py
new file mode 100644
--- /dev/null
+++ b/nautobot/core/tests/test_openapi.py
@@ -0,0 +1,64 @@
+from drf_spectacular.renderers import OpenApiYamlRenderer
+from drf_spectacular.settings import spectacular_settings
+
+import yaml
+
+from nautobot.core.testing import TestCase
+
+
+class OpenAPITest(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        generator_class = spectacular_settings.DEFAULT_GENERATOR_CLASS
+        generator = generator_class()  # TODO: in future we may want to specify an `api_version=...` here
+        schema = generator.get_schema(request=None, public=True)
+        # We probably could stop here but let's round-trip it through the YAML renderer just for the heck of it
+        renderer = OpenApiYamlRenderer()
+        cls.binary_output = renderer.render(schema, renderer_context={})
+        cls.yaml_output = cls.binary_output.decode("utf-8")
+        cls.schema = yaml.safe_load(cls.yaml_output)
+
+    def test_filter_boolean_type(self):
+        """
+        Test that a boolean filter is correctly represented as a boolean.
+
+        Testing for regression of https://github.com/nautobot/nautobot/issues/4377.
+        """
+        query_params = self.schema["paths"]["/dcim/devices/"]["get"]["parameters"]
+        at_least_one_test = False
+        for query_param_info in query_params:
+            if query_param_info["name"].startswith("has_"):
+                self.assertEqual("boolean", query_param_info["schema"]["type"])
+                at_least_one_test = True
+        self.assertTrue(at_least_one_test)
+
+    def test_filter_datetime_type(self):
+        """
+        Test that a datetime filter is correctly represented as an array of date-time strings.
+
+        Testing for regression of https://github.com/nautobot/nautobot/issues/4377.
+        """
+        query_params = self.schema["paths"]["/dcim/devices/"]["get"]["parameters"]
+        at_least_one_test = False
+        for query_param_info in query_params:
+            if query_param_info["name"].startswith("created") or query_param_info["name"].startswith("last_updated"):
+                self.assertEqual("array", query_param_info["schema"]["type"])
+                self.assertEqual("string", query_param_info["schema"]["items"]["type"])
+                self.assertEqual("date-time", query_param_info["schema"]["items"]["format"])
+                at_least_one_test = True
+        self.assertTrue(at_least_one_test)
+
+    def test_filter_integer_type(self):
+        """
+        Test that an integer filter is correctly represented as an array of integers.
+
+        Testing for regression of https://github.com/nautobot/nautobot/issues/4377.
+        """
+        query_params = self.schema["paths"]["/dcim/devices/"]["get"]["parameters"]
+        at_least_one_test = False
+        for query_param_info in query_params:
+            if query_param_info["name"].startswith("device_redundancy_group_priority"):
+                self.assertEqual("array", query_param_info["schema"]["type"])
+                self.assertEqual("integer", query_param_info["schema"]["items"]["type"])
+                at_least_one_test = True
+        self.assertTrue(at_least_one_test)