We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Values for Selection Custom Fields via API
<!--
    Before opening a new issue, please search through the existing issues to
    see if your topic has already been addressed. Note that you may need to
    remove the "is:open" filter from the search bar to include closed issues.

    Check the appropriate type for your issue below by placing an x between the
    brackets. For assistance with installation issues, or for any other issues
    other than those listed below, please raise your topic for discussion on
    our mailing list:

        https://groups.google.com/forum/#!forum/netbox-discuss

    Please note that issues which do not fall under any of the below categories
    will be closed. Due to an excessive backlog of feature requests, we are
    not currently accepting any proposals which extend NetBox's feature scope.

    Do not prepend any sort of tag to your issue's title. An administrator will
    review your issue and assign labels as appropriate.
--->
### Issue type
[X] Feature request <!-- An enhancement of existing functionality -->
[ ] Bug report      <!-- Unexpected or erroneous behavior -->
[ ] Documentation   <!-- A modification to the documentation -->

<!--
    Please describe the environment in which you are running NetBox. (Be sure
    to verify that you are running the latest stable release of NetBox before
    submitting a bug report.) If you are submitting a bug report and have made
    any changes to the code base, please first validate that your bug can be
    recreated while running an official release.
-->
### Environment
* Python version:  3.6
* NetBox version:  2.2.8

<!--
    BUG REPORTS must include:
        * A list of the steps needed for someone else to reproduce the bug
        * A description of the expected and observed behavior
        * Any relevant error messages (screenshots may also help)

    FEATURE REQUESTS must include:
        * A detailed description of the proposed functionality
        * A use case for the new feature
        * A rough description of any necessary changes to the database schema
        * Any relevant third-party libraries which would be needed
-->
### Description

As I [already told on the mailing list](https://groups.google.com/forum/#!topic/netbox-discuss/4j7ODlKWNk0), I would like to fill in the values of _selection custom fields_ via the API.

I expected to provide the actual value of the field, i.e. the text I see in the dropdown of the GUI. But the API expects to get the ID of the option, rather than the value, and there seems to be no way of looking up the available (value,id)-pairs for a given _selection custom fields_. Also I would like to avoid hardcoding any (value,id)-pairs in my application, as it's not consistent across our different Netbox instances (local, staging, production) and it would be very fragile.

I see two solutions:
* When I set the value of such a selection custom field, Netbox accepts the String value and converts it to the respective ID internally.
* There is a new endpoint to fetch the option pairs of selection custom fields, similar to the other __options_ endpoints.

IMO both options could even live in parallel to each other: Whenever an integer is presented as value for a selection custom field, we can assume it's the ID of the value. Whenever it's a string, we can assume it's the text value and first has to be converted to the corresponding integer id.

Allowing to submit text values via the API instead of the actual IDs might imply that we don't allow the same text value on one given selection custom field twice.
  

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netbox/extras/api/views.py`
Content:
```
1 from django.contrib.contenttypes.models import ContentType
2 from django.db.models import Count
3 from django.http import Http404, HttpResponse
4 from django.shortcuts import get_object_or_404
5 from rest_framework.decorators import action
6 from rest_framework.exceptions import PermissionDenied
7 from rest_framework.response import Response
8 from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
9 
10 from extras import filters
11 from extras.models import (
12     ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
13     Tag
14 )
15 from extras.reports import get_report, get_reports
16 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
17 from . import serializers
18 
19 
20 #
21 # Field choices
22 #
23 
24 class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
25     fields = (
26         (CustomField, ['type']),
27         (Graph, ['type']),
28     )
29 
30 
31 #
32 # Custom fields
33 #
34 
35 class CustomFieldModelViewSet(ModelViewSet):
36     """
37     Include the applicable set of CustomFields in the ModelViewSet context.
38     """
39 
40     def get_serializer_context(self):
41 
42         # Gather all custom fields for the model
43         content_type = ContentType.objects.get_for_model(self.queryset.model)
44         custom_fields = content_type.custom_fields.prefetch_related('choices')
45 
46         # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.
47         custom_field_choices = {}
48         for field in custom_fields:
49             for cfc in field.choices.all():
50                 custom_field_choices[cfc.id] = cfc.value
51         custom_field_choices = custom_field_choices
52 
53         context = super().get_serializer_context()
54         context.update({
55             'custom_fields': custom_fields,
56             'custom_field_choices': custom_field_choices,
57         })
58         return context
59 
60     def get_queryset(self):
61         # Prefetch custom field values
62         return super().get_queryset().prefetch_related('custom_field_values__field')
63 
64 
65 #
66 # Graphs
67 #
68 
69 class GraphViewSet(ModelViewSet):
70     queryset = Graph.objects.all()
71     serializer_class = serializers.GraphSerializer
72     filterset_class = filters.GraphFilter
73 
74 
75 #
76 # Export templates
77 #
78 
79 class ExportTemplateViewSet(ModelViewSet):
80     queryset = ExportTemplate.objects.all()
81     serializer_class = serializers.ExportTemplateSerializer
82     filterset_class = filters.ExportTemplateFilter
83 
84 
85 #
86 # Topology maps
87 #
88 
89 class TopologyMapViewSet(ModelViewSet):
90     queryset = TopologyMap.objects.select_related('site')
91     serializer_class = serializers.TopologyMapSerializer
92     filterset_class = filters.TopologyMapFilter
93 
94     @action(detail=True)
95     def render(self, request, pk):
96 
97         tmap = get_object_or_404(TopologyMap, pk=pk)
98         img_format = 'png'
99 
100         try:
101             data = tmap.render(img_format=img_format)
102         except Exception as e:
103             return HttpResponse(
104                 "There was an error generating the requested graph: %s" % e
105             )
106 
107         response = HttpResponse(data, content_type='image/{}'.format(img_format))
108         response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format)
109 
110         return response
111 
112 
113 #
114 # Tags
115 #
116 
117 class TagViewSet(ModelViewSet):
118     queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items'))
119     serializer_class = serializers.TagSerializer
120     filterset_class = filters.TagFilter
121 
122 
123 #
124 # Image attachments
125 #
126 
127 class ImageAttachmentViewSet(ModelViewSet):
128     queryset = ImageAttachment.objects.all()
129     serializer_class = serializers.ImageAttachmentSerializer
130 
131 
132 #
133 # Config contexts
134 #
135 
136 class ConfigContextViewSet(ModelViewSet):
137     queryset = ConfigContext.objects.prefetch_related(
138         'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
139     )
140     serializer_class = serializers.ConfigContextSerializer
141     filterset_class = filters.ConfigContextFilter
142 
143 
144 #
145 # Reports
146 #
147 
148 class ReportViewSet(ViewSet):
149     permission_classes = [IsAuthenticatedOrLoginNotRequired]
150     _ignore_model_permissions = True
151     exclude_from_schema = True
152     lookup_value_regex = '[^/]+'  # Allow dots
153 
154     def _retrieve_report(self, pk):
155 
156         # Read the PK as "<module>.<report>"
157         if '.' not in pk:
158             raise Http404
159         module_name, report_name = pk.split('.', 1)
160 
161         # Raise a 404 on an invalid Report module/name
162         report = get_report(module_name, report_name)
163         if report is None:
164             raise Http404
165 
166         return report
167 
168     def list(self, request):
169         """
170         Compile all reports and their related results (if any). Result data is deferred in the list view.
171         """
172         report_list = []
173 
174         # Iterate through all available Reports.
175         for module_name, reports in get_reports():
176             for report in reports:
177 
178                 # Attach the relevant ReportResult (if any) to each Report.
179                 report.result = ReportResult.objects.filter(report=report.full_name).defer('data').first()
180                 report_list.append(report)
181 
182         serializer = serializers.ReportSerializer(report_list, many=True, context={
183             'request': request,
184         })
185 
186         return Response(serializer.data)
187 
188     def retrieve(self, request, pk):
189         """
190         Retrieve a single Report identified as "<module>.<report>".
191         """
192 
193         # Retrieve the Report and ReportResult, if any.
194         report = self._retrieve_report(pk)
195         report.result = ReportResult.objects.filter(report=report.full_name).first()
196 
197         serializer = serializers.ReportDetailSerializer(report)
198 
199         return Response(serializer.data)
200 
201     @action(detail=True, methods=['post'])
202     def run(self, request, pk):
203         """
204         Run a Report and create a new ReportResult, overwriting any previous result for the Report.
205         """
206 
207         # Check that the user has permission to run reports.
208         if not request.user.has_perm('extras.add_reportresult'):
209             raise PermissionDenied("This user does not have permission to run reports.")
210 
211         # Retrieve and run the Report. This will create a new ReportResult.
212         report = self._retrieve_report(pk)
213         report.run()
214 
215         serializer = serializers.ReportDetailSerializer(report)
216 
217         return Response(serializer.data)
218 
219 
220 #
221 # Change logging
222 #
223 
224 class ObjectChangeViewSet(ReadOnlyModelViewSet):
225     """
226     Retrieve a list of recent changes.
227     """
228     queryset = ObjectChange.objects.select_related('user')
229     serializer_class = serializers.ObjectChangeSerializer
230     filterset_class = filters.ObjectChangeFilter
231
```

Path: `netbox/extras/api/urls.py`
Content:
```
1 from rest_framework import routers
2 
3 from . import views
4 
5 
6 class ExtrasRootView(routers.APIRootView):
7     """
8     Extras API root view
9     """
10     def get_view_name(self):
11         return 'Extras'
12 
13 
14 router = routers.DefaultRouter()
15 router.APIRootView = ExtrasRootView
16 
17 # Field choices
18 router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
19 
20 # Graphs
21 router.register(r'graphs', views.GraphViewSet)
22 
23 # Export templates
24 router.register(r'export-templates', views.ExportTemplateViewSet)
25 
26 # Topology maps
27 router.register(r'topology-maps', views.TopologyMapViewSet)
28 
29 # Tags
30 router.register(r'tags', views.TagViewSet)
31 
32 # Image attachments
33 router.register(r'image-attachments', views.ImageAttachmentViewSet)
34 
35 # Config contexts
36 router.register(r'config-contexts', views.ConfigContextViewSet)
37 
38 # Reports
39 router.register(r'reports', views.ReportViewSet, basename='report')
40 
41 # Change logging
42 router.register(r'object-changes', views.ObjectChangeViewSet)
43 
44 app_name = 'extras-api'
45 urlpatterns = router.urls
46
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py
--- a/netbox/extras/api/urls.py
+++ b/netbox/extras/api/urls.py
@@ -17,6 +17,9 @@
 # Field choices
 router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
 
+# Custom field choices
+router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')
+
 # Graphs
 router.register(r'graphs', views.GraphViewSet)
 
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -1,3 +1,5 @@
+from collections import OrderedDict
+
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Count
 from django.http import Http404, HttpResponse
@@ -9,8 +11,8 @@
 
 from extras import filters
 from extras.models import (
-    ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
-    Tag
+    ConfigContext, CustomField, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
+    Tag,
 )
 from extras.reports import get_report, get_reports
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
@@ -28,6 +30,36 @@
     )
 
 
+#
+# Custom field choices
+#
+
+class CustomFieldChoicesViewSet(ViewSet):
+    """
+    """
+    permission_classes = [IsAuthenticatedOrLoginNotRequired]
+
+    def __init__(self, *args, **kwargs):
+        super(CustomFieldChoicesViewSet, self).__init__(*args, **kwargs)
+
+        self._fields = OrderedDict()
+
+        for cfc in CustomFieldChoice.objects.all():
+            self._fields.setdefault(cfc.field.name, {})
+            self._fields[cfc.field.name][cfc.value] = cfc.pk
+
+    def list(self, request):
+        return Response(self._fields)
+
+    def retrieve(self, request, pk):
+        if pk not in self._fields:
+            raise Http404
+        return Response(self._fields[pk])
+
+    def get_view_name(self):
+        return "Custom Field choices"
+
+
 #
 # Custom fields
 #

{"golden_diff": "diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py\n--- a/netbox/extras/api/urls.py\n+++ b/netbox/extras/api/urls.py\n@@ -17,6 +17,9 @@\n # Field choices\n router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')\n \n+# Custom field choices\n+router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')\n+\n # Graphs\n router.register(r'graphs', views.GraphViewSet)\n \ndiff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py\n--- a/netbox/extras/api/views.py\n+++ b/netbox/extras/api/views.py\n@@ -1,3 +1,5 @@\n+from collections import OrderedDict\n+\n from django.contrib.contenttypes.models import ContentType\n from django.db.models import Count\n from django.http import Http404, HttpResponse\n@@ -9,8 +11,8 @@\n \n from extras import filters\n from extras.models import (\n-    ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,\n-    Tag\n+    ConfigContext, CustomField, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,\n+    Tag,\n )\n from extras.reports import get_report, get_reports\n from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet\n@@ -28,6 +30,36 @@\n     )\n \n \n+#\n+# Custom field choices\n+#\n+\n+class CustomFieldChoicesViewSet(ViewSet):\n+    \"\"\"\n+    \"\"\"\n+    permission_classes = [IsAuthenticatedOrLoginNotRequired]\n+\n+    def __init__(self, *args, **kwargs):\n+        super(CustomFieldChoicesViewSet, self).__init__(*args, **kwargs)\n+\n+        self._fields = OrderedDict()\n+\n+        for cfc in CustomFieldChoice.objects.all():\n+            self._fields.setdefault(cfc.field.name, {})\n+            self._fields[cfc.field.name][cfc.value] = cfc.pk\n+\n+    def list(self, request):\n+        return Response(self._fields)\n+\n+    def retrieve(self, request, pk):\n+        if pk not in self._fields:\n+            raise Http404\n+        return Response(self._fields[pk])\n+\n+    def get_view_name(self):\n+        return \"Custom Field choices\"\n+\n+\n #\n # Custom fields\n #\n", "issue": "Values for Selection Custom Fields via API\n<!--\r\n    Before opening a new issue, please search through the existing issues to\r\n    see if your topic has already been addressed. Note that you may need to\r\n    remove the \"is:open\" filter from the search bar to include closed issues.\r\n\r\n    Check the appropriate type for your issue below by placing an x between the\r\n    brackets. For assistance with installation issues, or for any other issues\r\n    other than those listed below, please raise your topic for discussion on\r\n    our mailing list:\r\n\r\n        https://groups.google.com/forum/#!forum/netbox-discuss\r\n\r\n    Please note that issues which do not fall under any of the below categories\r\n    will be closed. Due to an excessive backlog of feature requests, we are\r\n    not currently accepting any proposals which extend NetBox's feature scope.\r\n\r\n    Do not prepend any sort of tag to your issue's title. An administrator will\r\n    review your issue and assign labels as appropriate.\r\n--->\r\n### Issue type\r\n[X] Feature request <!-- An enhancement of existing functionality -->\r\n[ ] Bug report      <!-- Unexpected or erroneous behavior -->\r\n[ ] Documentation   <!-- A modification to the documentation -->\r\n\r\n<!--\r\n    Please describe the environment in which you are running NetBox. (Be sure\r\n    to verify that you are running the latest stable release of NetBox before\r\n    submitting a bug report.) If you are submitting a bug report and have made\r\n    any changes to the code base, please first validate that your bug can be\r\n    recreated while running an official release.\r\n-->\r\n### Environment\r\n* Python version:  3.6\r\n* NetBox version:  2.2.8\r\n\r\n<!--\r\n    BUG REPORTS must include:\r\n        * A list of the steps needed for someone else to reproduce the bug\r\n        * A description of the expected and observed behavior\r\n        * Any relevant error messages (screenshots may also help)\r\n\r\n    FEATURE REQUESTS must include:\r\n        * A detailed description of the proposed functionality\r\n        * A use case for the new feature\r\n        * A rough description of any necessary changes to the database schema\r\n        * Any relevant third-party libraries which would be needed\r\n-->\r\n### Description\r\n\r\nAs I [already told on the mailing list](https://groups.google.com/forum/#!topic/netbox-discuss/4j7ODlKWNk0), I would like to fill in the values of _selection custom fields_ via the API.\r\n\r\nI expected to provide the actual value of the field, i.e. the text I see in the dropdown of the GUI. But the API expects to get the ID of the option, rather than the value, and there seems to be no way of looking up the available (value,id)-pairs for a given _selection custom fields_. Also I would like to avoid hardcoding any (value,id)-pairs in my application, as it's not consistent across our different Netbox instances (local, staging, production) and it would be very fragile.\r\n\r\nI see two solutions:\r\n* When I set the value of such a selection custom field, Netbox accepts the String value and converts it to the respective ID internally.\r\n* There is a new endpoint to fetch the option pairs of selection custom fields, similar to the other __options_ endpoints.\r\n\r\nIMO both options could even live in parallel to each other: Whenever an integer is presented as value for a selection custom field, we can assume it's the ID of the value. Whenever it's a string, we can assume it's the text value and first has to be converted to the corresponding integer id.\r\n\r\nAllowing to submit text values via the API instead of the actual IDs might imply that we don't allow the same text value on one given selection custom field twice.\r\n  \n", "before_files": [{"content": "from django.contrib.contenttypes.models import ContentType\nfrom django.db.models import Count\nfrom django.http import Http404, HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom rest_framework.decorators import action\nfrom rest_framework.exceptions import PermissionDenied\nfrom rest_framework.response import Response\nfrom rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet\n\nfrom extras import filters\nfrom extras.models import (\n    ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,\n    Tag\n)\nfrom extras.reports import get_report, get_reports\nfrom utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet\nfrom . import serializers\n\n\n#\n# Field choices\n#\n\nclass ExtrasFieldChoicesViewSet(FieldChoicesViewSet):\n    fields = (\n        (CustomField, ['type']),\n        (Graph, ['type']),\n    )\n\n\n#\n# Custom fields\n#\n\nclass CustomFieldModelViewSet(ModelViewSet):\n    \"\"\"\n    Include the applicable set of CustomFields in the ModelViewSet context.\n    \"\"\"\n\n    def get_serializer_context(self):\n\n        # Gather all custom fields for the model\n        content_type = ContentType.objects.get_for_model(self.queryset.model)\n        custom_fields = content_type.custom_fields.prefetch_related('choices')\n\n        # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.\n        custom_field_choices = {}\n        for field in custom_fields:\n            for cfc in field.choices.all():\n                custom_field_choices[cfc.id] = cfc.value\n        custom_field_choices = custom_field_choices\n\n        context = super().get_serializer_context()\n        context.update({\n            'custom_fields': custom_fields,\n            'custom_field_choices': custom_field_choices,\n        })\n        return context\n\n    def get_queryset(self):\n        # Prefetch custom field values\n        return super().get_queryset().prefetch_related('custom_field_values__field')\n\n\n#\n# Graphs\n#\n\nclass GraphViewSet(ModelViewSet):\n    queryset = Graph.objects.all()\n    serializer_class = serializers.GraphSerializer\n    filterset_class = filters.GraphFilter\n\n\n#\n# Export templates\n#\n\nclass ExportTemplateViewSet(ModelViewSet):\n    queryset = ExportTemplate.objects.all()\n    serializer_class = serializers.ExportTemplateSerializer\n    filterset_class = filters.ExportTemplateFilter\n\n\n#\n# Topology maps\n#\n\nclass TopologyMapViewSet(ModelViewSet):\n    queryset = TopologyMap.objects.select_related('site')\n    serializer_class = serializers.TopologyMapSerializer\n    filterset_class = filters.TopologyMapFilter\n\n    @action(detail=True)\n    def render(self, request, pk):\n\n        tmap = get_object_or_404(TopologyMap, pk=pk)\n        img_format = 'png'\n\n        try:\n            data = tmap.render(img_format=img_format)\n        except Exception as e:\n            return HttpResponse(\n                \"There was an error generating the requested graph: %s\" % e\n            )\n\n        response = HttpResponse(data, content_type='image/{}'.format(img_format))\n        response['Content-Disposition'] = 'inline; filename=\"{}.{}\"'.format(tmap.slug, img_format)\n\n        return response\n\n\n#\n# Tags\n#\n\nclass TagViewSet(ModelViewSet):\n    queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items'))\n    serializer_class = serializers.TagSerializer\n    filterset_class = filters.TagFilter\n\n\n#\n# Image attachments\n#\n\nclass ImageAttachmentViewSet(ModelViewSet):\n    queryset = ImageAttachment.objects.all()\n    serializer_class = serializers.ImageAttachmentSerializer\n\n\n#\n# Config contexts\n#\n\nclass ConfigContextViewSet(ModelViewSet):\n    queryset = ConfigContext.objects.prefetch_related(\n        'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',\n    )\n    serializer_class = serializers.ConfigContextSerializer\n    filterset_class = filters.ConfigContextFilter\n\n\n#\n# Reports\n#\n\nclass ReportViewSet(ViewSet):\n    permission_classes = [IsAuthenticatedOrLoginNotRequired]\n    _ignore_model_permissions = True\n    exclude_from_schema = True\n    lookup_value_regex = '[^/]+'  # Allow dots\n\n    def _retrieve_report(self, pk):\n\n        # Read the PK as \"<module>.<report>\"\n        if '.' not in pk:\n            raise Http404\n        module_name, report_name = pk.split('.', 1)\n\n        # Raise a 404 on an invalid Report module/name\n        report = get_report(module_name, report_name)\n        if report is None:\n            raise Http404\n\n        return report\n\n    def list(self, request):\n        \"\"\"\n        Compile all reports and their related results (if any). Result data is deferred in the list view.\n        \"\"\"\n        report_list = []\n\n        # Iterate through all available Reports.\n        for module_name, reports in get_reports():\n            for report in reports:\n\n                # Attach the relevant ReportResult (if any) to each Report.\n                report.result = ReportResult.objects.filter(report=report.full_name).defer('data').first()\n                report_list.append(report)\n\n        serializer = serializers.ReportSerializer(report_list, many=True, context={\n            'request': request,\n        })\n\n        return Response(serializer.data)\n\n    def retrieve(self, request, pk):\n        \"\"\"\n        Retrieve a single Report identified as \"<module>.<report>\".\n        \"\"\"\n\n        # Retrieve the Report and ReportResult, if any.\n        report = self._retrieve_report(pk)\n        report.result = ReportResult.objects.filter(report=report.full_name).first()\n\n        serializer = serializers.ReportDetailSerializer(report)\n\n        return Response(serializer.data)\n\n    @action(detail=True, methods=['post'])\n    def run(self, request, pk):\n        \"\"\"\n        Run a Report and create a new ReportResult, overwriting any previous result for the Report.\n        \"\"\"\n\n        # Check that the user has permission to run reports.\n        if not request.user.has_perm('extras.add_reportresult'):\n            raise PermissionDenied(\"This user does not have permission to run reports.\")\n\n        # Retrieve and run the Report. This will create a new ReportResult.\n        report = self._retrieve_report(pk)\n        report.run()\n\n        serializer = serializers.ReportDetailSerializer(report)\n\n        return Response(serializer.data)\n\n\n#\n# Change logging\n#\n\nclass ObjectChangeViewSet(ReadOnlyModelViewSet):\n    \"\"\"\n    Retrieve a list of recent changes.\n    \"\"\"\n    queryset = ObjectChange.objects.select_related('user')\n    serializer_class = serializers.ObjectChangeSerializer\n    filterset_class = filters.ObjectChangeFilter\n", "path": "netbox/extras/api/views.py"}, {"content": "from rest_framework import routers\n\nfrom . import views\n\n\nclass ExtrasRootView(routers.APIRootView):\n    \"\"\"\n    Extras API root view\n    \"\"\"\n    def get_view_name(self):\n        return 'Extras'\n\n\nrouter = routers.DefaultRouter()\nrouter.APIRootView = ExtrasRootView\n\n# Field choices\nrouter.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')\n\n# Graphs\nrouter.register(r'graphs', views.GraphViewSet)\n\n# Export templates\nrouter.register(r'export-templates', views.ExportTemplateViewSet)\n\n# Topology maps\nrouter.register(r'topology-maps', views.TopologyMapViewSet)\n\n# Tags\nrouter.register(r'tags', views.TagViewSet)\n\n# Image attachments\nrouter.register(r'image-attachments', views.ImageAttachmentViewSet)\n\n# Config contexts\nrouter.register(r'config-contexts', views.ConfigContextViewSet)\n\n# Reports\nrouter.register(r'reports', views.ReportViewSet, basename='report')\n\n# Change logging\nrouter.register(r'object-changes', views.ObjectChangeViewSet)\n\napp_name = 'extras-api'\nurlpatterns = router.urls\n", "path": "netbox/extras/api/urls.py"}], "after_files": [{"content": "from collections import OrderedDict\n\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db.models import Count\nfrom django.http import Http404, HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom rest_framework.decorators import action\nfrom rest_framework.exceptions import PermissionDenied\nfrom rest_framework.response import Response\nfrom rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet\n\nfrom extras import filters\nfrom extras.models import (\n    ConfigContext, CustomField, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,\n    Tag,\n)\nfrom extras.reports import get_report, get_reports\nfrom utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet\nfrom . import serializers\n\n\n#\n# Field choices\n#\n\nclass ExtrasFieldChoicesViewSet(FieldChoicesViewSet):\n    fields = (\n        (CustomField, ['type']),\n        (Graph, ['type']),\n    )\n\n\n#\n# Custom field choices\n#\n\nclass CustomFieldChoicesViewSet(ViewSet):\n    \"\"\"\n    \"\"\"\n    permission_classes = [IsAuthenticatedOrLoginNotRequired]\n\n    def __init__(self, *args, **kwargs):\n        super(CustomFieldChoicesViewSet, self).__init__(*args, **kwargs)\n\n        self._fields = OrderedDict()\n\n        for cfc in CustomFieldChoice.objects.all():\n            self._fields.setdefault(cfc.field.name, {})\n            self._fields[cfc.field.name][cfc.value] = cfc.pk\n\n    def list(self, request):\n        return Response(self._fields)\n\n    def retrieve(self, request, pk):\n        if pk not in self._fields:\n            raise Http404\n        return Response(self._fields[pk])\n\n    def get_view_name(self):\n        return \"Custom Field choices\"\n\n\n#\n# Custom fields\n#\n\nclass CustomFieldModelViewSet(ModelViewSet):\n    \"\"\"\n    Include the applicable set of CustomFields in the ModelViewSet context.\n    \"\"\"\n\n    def get_serializer_context(self):\n\n        # Gather all custom fields for the model\n        content_type = ContentType.objects.get_for_model(self.queryset.model)\n        custom_fields = content_type.custom_fields.prefetch_related('choices')\n\n        # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.\n        custom_field_choices = {}\n        for field in custom_fields:\n            for cfc in field.choices.all():\n                custom_field_choices[cfc.id] = cfc.value\n        custom_field_choices = custom_field_choices\n\n        context = super().get_serializer_context()\n        context.update({\n            'custom_fields': custom_fields,\n            'custom_field_choices': custom_field_choices,\n        })\n        return context\n\n    def get_queryset(self):\n        # Prefetch custom field values\n        return super().get_queryset().prefetch_related('custom_field_values__field')\n\n\n#\n# Graphs\n#\n\nclass GraphViewSet(ModelViewSet):\n    queryset = Graph.objects.all()\n    serializer_class = serializers.GraphSerializer\n    filterset_class = filters.GraphFilter\n\n\n#\n# Export templates\n#\n\nclass ExportTemplateViewSet(ModelViewSet):\n    queryset = ExportTemplate.objects.all()\n    serializer_class = serializers.ExportTemplateSerializer\n    filterset_class = filters.ExportTemplateFilter\n\n\n#\n# Topology maps\n#\n\nclass TopologyMapViewSet(ModelViewSet):\n    queryset = TopologyMap.objects.select_related('site')\n    serializer_class = serializers.TopologyMapSerializer\n    filterset_class = filters.TopologyMapFilter\n\n    @action(detail=True)\n    def render(self, request, pk):\n\n        tmap = get_object_or_404(TopologyMap, pk=pk)\n        img_format = 'png'\n\n        try:\n            data = tmap.render(img_format=img_format)\n        except Exception as e:\n            return HttpResponse(\n                \"There was an error generating the requested graph: %s\" % e\n            )\n\n        response = HttpResponse(data, content_type='image/{}'.format(img_format))\n        response['Content-Disposition'] = 'inline; filename=\"{}.{}\"'.format(tmap.slug, img_format)\n\n        return response\n\n\n#\n# Tags\n#\n\nclass TagViewSet(ModelViewSet):\n    queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items'))\n    serializer_class = serializers.TagSerializer\n    filterset_class = filters.TagFilter\n\n\n#\n# Image attachments\n#\n\nclass ImageAttachmentViewSet(ModelViewSet):\n    queryset = ImageAttachment.objects.all()\n    serializer_class = serializers.ImageAttachmentSerializer\n\n\n#\n# Config contexts\n#\n\nclass ConfigContextViewSet(ModelViewSet):\n    queryset = ConfigContext.objects.prefetch_related(\n        'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',\n    )\n    serializer_class = serializers.ConfigContextSerializer\n    filterset_class = filters.ConfigContextFilter\n\n\n#\n# Reports\n#\n\nclass ReportViewSet(ViewSet):\n    permission_classes = [IsAuthenticatedOrLoginNotRequired]\n    _ignore_model_permissions = True\n    exclude_from_schema = True\n    lookup_value_regex = '[^/]+'  # Allow dots\n\n    def _retrieve_report(self, pk):\n\n        # Read the PK as \"<module>.<report>\"\n        if '.' not in pk:\n            raise Http404\n        module_name, report_name = pk.split('.', 1)\n\n        # Raise a 404 on an invalid Report module/name\n        report = get_report(module_name, report_name)\n        if report is None:\n            raise Http404\n\n        return report\n\n    def list(self, request):\n        \"\"\"\n        Compile all reports and their related results (if any). Result data is deferred in the list view.\n        \"\"\"\n        report_list = []\n\n        # Iterate through all available Reports.\n        for module_name, reports in get_reports():\n            for report in reports:\n\n                # Attach the relevant ReportResult (if any) to each Report.\n                report.result = ReportResult.objects.filter(report=report.full_name).defer('data').first()\n                report_list.append(report)\n\n        serializer = serializers.ReportSerializer(report_list, many=True, context={\n            'request': request,\n        })\n\n        return Response(serializer.data)\n\n    def retrieve(self, request, pk):\n        \"\"\"\n        Retrieve a single Report identified as \"<module>.<report>\".\n        \"\"\"\n\n        # Retrieve the Report and ReportResult, if any.\n        report = self._retrieve_report(pk)\n        report.result = ReportResult.objects.filter(report=report.full_name).first()\n\n        serializer = serializers.ReportDetailSerializer(report)\n\n        return Response(serializer.data)\n\n    @action(detail=True, methods=['post'])\n    def run(self, request, pk):\n        \"\"\"\n        Run a Report and create a new ReportResult, overwriting any previous result for the Report.\n        \"\"\"\n\n        # Check that the user has permission to run reports.\n        if not request.user.has_perm('extras.add_reportresult'):\n            raise PermissionDenied(\"This user does not have permission to run reports.\")\n\n        # Retrieve and run the Report. This will create a new ReportResult.\n        report = self._retrieve_report(pk)\n        report.run()\n\n        serializer = serializers.ReportDetailSerializer(report)\n\n        return Response(serializer.data)\n\n\n#\n# Change logging\n#\n\nclass ObjectChangeViewSet(ReadOnlyModelViewSet):\n    \"\"\"\n    Retrieve a list of recent changes.\n    \"\"\"\n    queryset = ObjectChange.objects.select_related('user')\n    serializer_class = serializers.ObjectChangeSerializer\n    filterset_class = filters.ObjectChangeFilter\n", "path": "netbox/extras/api/views.py"}, {"content": "from rest_framework import routers\n\nfrom . import views\n\n\nclass ExtrasRootView(routers.APIRootView):\n    \"\"\"\n    Extras API root view\n    \"\"\"\n    def get_view_name(self):\n        return 'Extras'\n\n\nrouter = routers.DefaultRouter()\nrouter.APIRootView = ExtrasRootView\n\n# Field choices\nrouter.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')\n\n# Custom field choices\nrouter.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')\n\n# Graphs\nrouter.register(r'graphs', views.GraphViewSet)\n\n# Export templates\nrouter.register(r'export-templates', views.ExportTemplateViewSet)\n\n# Topology maps\nrouter.register(r'topology-maps', views.TopologyMapViewSet)\n\n# Tags\nrouter.register(r'tags', views.TagViewSet)\n\n# Image attachments\nrouter.register(r'image-attachments', views.ImageAttachmentViewSet)\n\n# Config contexts\nrouter.register(r'config-contexts', views.ConfigContextViewSet)\n\n# Reports\nrouter.register(r'reports', views.ReportViewSet, basename='report')\n\n# Change logging\nrouter.register(r'object-changes', views.ObjectChangeViewSet)\n\napp_name = 'extras-api'\nurlpatterns = router.urls\n", "path": "netbox/extras/api/urls.py"}]}