Repository: nautobot/nautobot

Issue:
OPTIONS returns all Status choices instead of ones only related to the specific model
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.8
* Nautobot version: 1.0.1

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
Cables should only have three status choices and virtual machines should have 6. Numbers are arbitrary, but this is with a fresh install of Nautobot.

```python
>>> nb.dcim.cables.choices()['status']
[{'value': 'active', 'display': 'Active'}, {'value': 'available', 'display': 'Available'}, {'value': 'connected', 'display': 'Connected'}, {'value': 'container', 'display': 'Container'}, {'value': 'decommissioned', 'display': 'Decommissioned'}, {'value': 'decommissioning', 'display': 'Decommissioning'}, {'value': 'deprecated', 'display': 'Deprecated'}, {'value': 'deprovisioning', 'display': 'Deprovisioning'}, {'value': 'dhcp', 'display': 'DHCP'}, {'value': 'failed', 'display': 'Failed'}, {'value': 'inventory', 'display': 'Inventory'}, {'value': 'offline', 'display': 'Offline'}, {'value': 'planned', 'display': 'Planned'}, {'value': 'provisioning', 'display': 'Provisioning'}, {'value': 'reserved', 'display': 'Reserved'}, {'value': 'retired', 'display': 'Retired'}, {'value': 'slaac', 'display': 'SLAAC'}, {'value': 'staged', 'display': 'Staged'}, {'value': 'staging', 'display': 'Staging'}]
>>> nb.virtualization.virtual_machines.choices()['status']
[{'value': 'active', 'display': 'Active'}, {'value': 'available', 'display': 'Available'}, {'value': 'connected', 'display': 'Connected'}, {'value': 'container', 'display': 'Container'}, {'value': 'decommissioned', 'display': 'Decommissioned'}, {'value': 'decommissioning', 'display': 'Decommissioning'}, {'value': 'deprecated', 'display': 'Deprecated'}, {'value': 'deprovisioning', 'display': 'Deprovisioning'}, {'value': 'dhcp', 'display': 'DHCP'}, {'value': 'failed', 'display': 'Failed'}, {'value': 'inventory', 'display': 'Inventory'}, {'value': 'offline', 'display': 'Offline'}, {'value': 'planned', 'display': 'Planned'}, {'value': 'provisioning', 'display': 'Provisioning'}, {'value': 'reserved', 'display': 'Reserved'}, {'value': 'retired', 'display': 'Retired'}, {'value': 'slaac', 'display': 'SLAAC'}, {'value': 'staged', 'display': 'Staged'}, {'value': 'staging', 'display': 'Staging'}]
```

I haven't been able to dig into it too much, but I believe this is the culprit.

https://github.com/nautobot/nautobot/blob/develop/nautobot/extras/api/serializers.py#L621

<!-- What did you expect to happen? -->
### Expected Behavior

```python
>>> nb.dcim.cables.choices()['status']
[{'value': 'connected', 'display': 'Connected'}, {'value': 'decommissioning', 'display': 'Decommissioning'}, {'value': 'planned', 'display': 'Planned'}]
>>> nb.virtualization.virtual_machines.choices()['status']
[{'value': 'active', 'display': 'Active'}, {'value': 'decommissioning', 'display': 'Decommissioning'}, {'value': 'failed', 'display': 'Failed'}, {'value': 'offline', 'display': 'Offline'}, {'value': 'planned', 'display': 'Planned'}, {'value': 'staged', 'display': 'Staged'}]
```

<!-- What happened instead? -->
### Observed Behavior
See **Steps to Reproduce**.


Discussion:
> It should be possible to do something similar on the serializer field to what the form field does with `StatusField.get_limit_choices_to()` - looks like creating a custom `get_queryset()` method on `StatusSerializerField` would be the equivalent approach.

Resolving patch:
diff --git a/nautobot/extras/api/fields.py b/nautobot/extras/api/fields.py
--- a/nautobot/extras/api/fields.py
+++ b/nautobot/extras/api/fields.py
@@ -38,6 +38,12 @@ def to_internal_value(self, data):
             data = data.lower()
         return super().to_internal_value(data)
 
+    def get_queryset(self):
+        """Only emit status options for this model/field combination."""
+        queryset = super().get_queryset()
+        model = self.parent.Meta.model
+        return queryset.get_for_model(model)
+
     def get_choices(self, cutoff=None):
         """
         Return a nested list of dicts for enum choices.


Test patch:
diff --git a/nautobot/utilities/testing/api.py b/nautobot/utilities/testing/api.py
--- a/nautobot/utilities/testing/api.py
+++ b/nautobot/utilities/testing/api.py
@@ -256,6 +256,32 @@ def test_options_returns_expected_choices(self):
 
             self.assertEqual(set(self.choices_fields), field_choices)
 
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+        def test_status_options_returns_expected_choices(self):
+            # Set to self.choices_fields as empty set to compare classes that shouldn't have any choice fields on serializer.
+            if not self.choices_fields:
+                self.choices_fields = set()
+
+            # Don't bother testing if there's no `status` field.
+            if "status" not in self.choices_fields:
+                self.skipTest("Object does not contain a `status` field.")
+
+            # Save self.user as superuser to be able to view available choices on list views.
+            self.user.is_superuser = True
+            self.user.save()
+
+            response = self.client.options(self._get_list_url(), **self.header)
+            actions = response.json()["actions"]["POST"]
+            choices = actions["status"]["choices"]
+
+            # Import Status here to avoid circular import issues w/ test utilities.
+            from nautobot.extras.models import Status  # noqa
+
+            # Assert that the expected Status objects matches what is emitted.
+            statuses = Status.objects.get_for_model(self.model)
+            expected = [{"value": v, "display": d} for (v, d) in statuses.values_list("slug", "name")]
+            self.assertListEqual(choices, expected)
+
     class CreateObjectViewTestCase(APITestCase):
         create_data = []
         validation_excluded_fields = []