Repository: netbox-community/netbox

Issue:
Expose choices of cable:termination_a_type and cable:termination_b_type
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for proposing specific new features or enhancements.
    If you have a general idea or question, please post to our mailing list
    instead of opening an issue:

        https://groups.google.com/forum/#!forum/netbox-discuss

    NOTE: Due to an excessive backlog of feature requests, we are not currently
    accepting any proposals which significantly extend NetBox's feature scope.

    Please describe the environment in which you are running NetBox. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report.
-->
### Environment
* Python version:  3.8.3
* NetBox version:  2.8.4

<!--
    Describe in detail the new functionality you are proposing. Include any
    specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
In older Netbox versions it was possible to get choices for cable:termination_a_type via this api-request:
```
GET '/api/dcim/_choices/'
...
"cable:termination_a_type": [
    {
      "value": "dcim.consoleport",
      "label": "console port"
    },
    {
      "value": "dcim.consoleserverport",
      "label": "console server port"
    },
    {
      "value": "dcim.interface",
      "label": "interface"
    },
...
```

But since #3416 it is not possible to get the choices for termination_a_type with the new OPTIONS request, as there are no choices listed:
```
OPTIONS '/api/dcim/cables/'
...
"termination_a_type": {
  "type": "field",
  "required": true,
  "read_only": false,
  "label": "Termination a type"
},
...
```

### Use Case
API users/developers would know which terminations exist.

### Database Changes
None

### External Dependencies
None


Discussion:
@christianharendt or @sdktr would one of you like to volunteer for this?
If time is not an issue I’ll take a shot at it

> @christianharendt or @sdktr would one of you like to volunteer for this?


> If time is not an issue I’ll take a shot at it

Thanks @sdktr for taking the lead. If I can assist you, please let me know.
@sdktr Can you provide an update on this please?
This still needs an owner, it seems.
Sorry that I missed this. If an additional couple of weeks are possible I’d still try and take an attempt at this.
It is alright, I think I have a solution for this

Resolving patch:
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -29,6 +29,7 @@
     get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
 )
 from utilities.utils import get_subquery
+from utilities.metadata import ContentTypeMetadata
 from virtualization.models import VirtualMachine
 from . import serializers
 from .exceptions import MissingFilterException
@@ -567,6 +568,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
 #
 
 class CableViewSet(ModelViewSet):
+    metadata_class = ContentTypeMetadata
     queryset = Cable.objects.prefetch_related(
         'termination_a', 'termination_b'
     )
diff --git a/netbox/utilities/metadata.py b/netbox/utilities/metadata.py
new file mode 100644
--- /dev/null
+++ b/netbox/utilities/metadata.py
@@ -0,0 +1,19 @@
+from rest_framework.metadata import SimpleMetadata
+from django.utils.encoding import force_str
+from utilities.api import ContentTypeField
+
+
+class ContentTypeMetadata(SimpleMetadata):
+
+    def get_field_info(self, field):
+        field_info = super().get_field_info(field)
+        if hasattr(field, 'queryset') and not field_info.get('read_only') and isinstance(field, ContentTypeField):
+            field_info['choices'] = [
+                {
+                    'value': choice_value,
+                    'display_name': force_str(choice_name, strings_only=True)
+                }
+                for choice_value, choice_name in field.choices.items()
+            ]
+            field_info['choices'].sort(key=lambda item: item['display_name'])
+        return field_info


Test patch: