Merge pull request #4359 from netbox-community/3416-remove-API-choices

Closes #3416: Remove API _choices endpoints
This commit is contained in:
Jeremy Stretch 2020-03-12 11:29:31 -04:00 committed by GitHub
commit 16bc262a4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 37 additions and 411 deletions

View File

@ -187,37 +187,6 @@ GET /api/ipam/prefixes/13980/?brief=1
The brief format is supported for both lists and individual objects.
### Static Choice Fields
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
Each choice includes a human-friendly label and its corresponding numeric value. For example, `GET /api/ipam/_choices/prefix:status/` will return:
```
[
{
"value": 0,
"label": "Container"
},
{
"value": 1,
"label": "Active"
},
{
"value": 2,
"label": "Reserved"
},
{
"value": 3,
"label": "Deprecated"
}
]
```
Thus, to set a prefix's status to "Reserved," it would be assigned the integer `2`.
A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app.
## Pagination
API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes:
@ -280,27 +249,32 @@ A list of objects retrieved via the API can be filtered by passing one or more q
GET /api/ipam/prefixes/?status=1
```
The choices available for fixed choice fields such as `status` are exposed in the API under a special `_choices` endpoint for each NetBox app. For example, the available choices for `Prefix.status` are listed at `/api/ipam/_choices/` under the key `prefix:status`:
The choices available for fixed choice fields such as `status` can be retrieved by sending an `OPTIONS` API request for the desired endpoint:
```no-highlight
$ curl -s -X OPTIONS \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://localhost:8000/api/ipam/prefixes/ | jq ".actions.POST.status.choices"
[
{
"value": "container",
"display_name": "Container"
},
{
"value": "active",
"display_name": "Active"
},
{
"value": "reserved",
"display_name": "Reserved"
},
{
"value": "deprecated",
"display_name": "Deprecated"
}
]
```
"prefix:status": [
{
"label": "Container",
"value": 0
},
{
"label": "Active",
"value": 1
},
{
"label": "Reserved",
"value": 2
},
{
"label": "Deprecated",
"value": 3
}
],
```
For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar".

View File

@ -14,9 +14,6 @@ class CircuitsRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = CircuitsRootView
# Field choices
router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
# Providers
router.register('providers', views.ProviderViewSet)

View File

@ -8,21 +8,10 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.api import ModelViewSet
from . import serializers
#
# Field choices
#
class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.CircuitSerializer, ['status']),
(serializers.CircuitTerminationSerializer, ['term_side']),
)
#
# Providers
#

View File

@ -6,7 +6,7 @@ from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site
from extras.models import Graph
from utilities.testing import APITestCase, choices_to_dict
from utilities.testing import APITestCase
class AppTest(APITestCase):
@ -18,19 +18,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('circuits-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# Circuit
self.assertEqual(choices_to_dict(response.data.get('circuit:status')), CircuitStatusChoices.as_dict())
# CircuitTermination
self.assertEqual(choices_to_dict(response.data.get('circuit-termination:term_side')), CircuitTerminationSideChoices.as_dict())
class ProviderTest(APITestCase):

View File

@ -14,9 +14,6 @@ class DCIMRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = DCIMRootView
# Field choices
router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
# Sites
router.register('regions', views.RegionViewSet)
router.register('sites', views.SiteViewSet)

View File

@ -26,7 +26,7 @@ from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph
from ipam.models import Prefix, VLAN
from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
)
from utilities.utils import get_subquery
from virtualization.models import VirtualMachine
@ -34,35 +34,6 @@ from . import serializers
from .exceptions import MissingFilterException
#
# Field choices
#
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(serializers.ConsolePortSerializer, ['type', 'connection_status']),
(serializers.ConsolePortTemplateSerializer, ['type']),
(serializers.ConsoleServerPortSerializer, ['type']),
(serializers.ConsoleServerPortTemplateSerializer, ['type']),
(serializers.DeviceSerializer, ['face', 'status']),
(serializers.DeviceTypeSerializer, ['subdevice_role']),
(serializers.FrontPortSerializer, ['type']),
(serializers.FrontPortTemplateSerializer, ['type']),
(serializers.InterfaceSerializer, ['type', 'mode']),
(serializers.InterfaceTemplateSerializer, ['type']),
(serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']),
(serializers.PowerOutletSerializer, ['type', 'feed_leg']),
(serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']),
(serializers.PowerPortSerializer, ['type', 'connection_status']),
(serializers.PowerPortTemplateSerializer, ['type']),
(serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']),
(serializers.RearPortSerializer, ['type']),
(serializers.RearPortTemplateSerializer, ['type']),
(serializers.SiteSerializer, ['status']),
)
# Mixins
class CableTraceMixin(object):

View File

@ -14,7 +14,7 @@ from dcim.models import (
)
from ipam.models import IPAddress, VLAN
from extras.models import Graph
from utilities.testing import APITestCase, choices_to_dict
from utilities.testing import APITestCase
from virtualization.models import Cluster, ClusterType
@ -27,79 +27,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('dcim-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# Cable
self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict())
content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS)
cable_termination_choices = {
"{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types
}
self.assertEqual(choices_to_dict(response.data.get('cable:termination_a_type')), cable_termination_choices)
self.assertEqual(choices_to_dict(response.data.get('cable:termination_b_type')), cable_termination_choices)
self.assertEqual(choices_to_dict(response.data.get('cable:type')), CableTypeChoices.as_dict())
# Console ports
self.assertEqual(choices_to_dict(response.data.get('console-port:type')), ConsolePortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('console-port:connection_status')), dict(CONNECTION_STATUS_CHOICES))
self.assertEqual(choices_to_dict(response.data.get('console-port-template:type')), ConsolePortTypeChoices.as_dict())
# Console server ports
self.assertEqual(choices_to_dict(response.data.get('console-server-port:type')), ConsolePortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('console-server-port-template:type')), ConsolePortTypeChoices.as_dict())
# Device
self.assertEqual(choices_to_dict(response.data.get('device:face')), DeviceFaceChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('device:status')), DeviceStatusChoices.as_dict())
# Device type
self.assertEqual(choices_to_dict(response.data.get('device-type:subdevice_role')), SubdeviceRoleChoices.as_dict())
# Front ports
self.assertEqual(choices_to_dict(response.data.get('front-port:type')), PortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('front-port-template:type')), PortTypeChoices.as_dict())
# Interfaces
self.assertEqual(choices_to_dict(response.data.get('interface:type')), InterfaceTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('interface:mode')), InterfaceModeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('interface-template:type')), InterfaceTypeChoices.as_dict())
# Power feed
self.assertEqual(choices_to_dict(response.data.get('power-feed:phase')), PowerFeedPhaseChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-feed:status')), PowerFeedStatusChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-feed:supply')), PowerFeedSupplyChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-feed:type')), PowerFeedTypeChoices.as_dict())
# Power outlets
self.assertEqual(choices_to_dict(response.data.get('power-outlet:type')), PowerOutletTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-outlet:feed_leg')), PowerOutletFeedLegChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:type')), PowerOutletTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:feed_leg')), PowerOutletFeedLegChoices.as_dict())
# Power ports
self.assertEqual(choices_to_dict(response.data.get('power-port:type')), PowerPortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('power-port:connection_status')), dict(CONNECTION_STATUS_CHOICES))
self.assertEqual(choices_to_dict(response.data.get('power-port-template:type')), PowerPortTypeChoices.as_dict())
# Rack
self.assertEqual(choices_to_dict(response.data.get('rack:type')), RackTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rack:width')), RackWidthChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rack:status')), RackStatusChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rack:outer_unit')), RackDimensionUnitChoices.as_dict())
# Rear ports
self.assertEqual(choices_to_dict(response.data.get('rear-port:type')), PortTypeChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('rear-port-template:type')), PortTypeChoices.as_dict())
# Site
self.assertEqual(choices_to_dict(response.data.get('site:status')), SiteStatusChoices.as_dict())
class RegionTest(APITestCase):

View File

@ -14,9 +14,6 @@ class ExtrasRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = ExtrasRootView
# Field choices
router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices
router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')

View File

@ -15,22 +15,10 @@ from extras.models import (
)
from extras.reports import get_report, get_reports
from extras.scripts import get_script, get_scripts, run_script
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
from utilities.api import IsAuthenticatedOrLoginNotRequired, ModelViewSet
from . import serializers
#
# Field choices
#
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.ExportTemplateSerializer, ['template_language']),
(serializers.GraphSerializer, ['type', 'template_language']),
(serializers.ObjectChangeSerializer, ['action']),
)
#
# Custom field choices
#

View File

@ -7,12 +7,10 @@ from rest_framework import status
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackRole, Region, Site
from extras.api.views import ScriptViewSet
from extras.choices import *
from extras.constants import GRAPH_MODELS
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from tenancy.models import Tenant, TenantGroup
from utilities.testing import APITestCase, choices_to_dict
from utilities.testing import APITestCase
class AppTest(APITestCase):
@ -24,27 +22,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('extras-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# ExportTemplate
self.assertEqual(choices_to_dict(response.data.get('export-template:template_language')), TemplateLanguageChoices.as_dict())
# Graph
content_types = ContentType.objects.filter(GRAPH_MODELS)
graph_type_choices = {
"{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types
}
self.assertEqual(choices_to_dict(response.data.get('graph:type')), graph_type_choices)
self.assertEqual(choices_to_dict(response.data.get('graph:template_language')), TemplateLanguageChoices.as_dict())
# ObjectChange
self.assertEqual(choices_to_dict(response.data.get('object-change:action')), ObjectChangeActionChoices.as_dict())
class GraphTest(APITestCase):

View File

@ -14,9 +14,6 @@ class IPAMRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = IPAMRootView
# Field choices
router.register('_choices', views.IPAMFieldChoicesViewSet, basename='field-choice')
# VRFs
router.register('vrfs', views.VRFViewSet)

View File

@ -10,26 +10,12 @@ from rest_framework.response import Response
from extras.api.views import CustomFieldModelViewSet
from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.api import ModelViewSet
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import get_subquery
from . import serializers
#
# Field choices
#
class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.AggregateSerializer, ['family']),
(serializers.PrefixSerializer, ['family', 'status']),
(serializers.IPAddressSerializer, ['family', 'status', 'role']),
(serializers.VLANSerializer, ['status']),
(serializers.ServiceSerializer, ['protocol']),
)
#
# VRFs
#

View File

@ -7,7 +7,7 @@ from rest_framework import status
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.choices import *
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.testing import APITestCase, choices_to_dict, disable_warnings
from utilities.testing import APITestCase, disable_warnings
class AppTest(APITestCase):
@ -19,31 +19,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('ipam-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# Aggregate
# self.assertEqual(choices_to_dict(response.data.get('aggregate:family')), )
# Prefix
# self.assertEqual(choices_to_dict(response.data.get('prefix:family')), )
self.assertEqual(choices_to_dict(response.data.get('prefix:status')), PrefixStatusChoices.as_dict())
# IPAddress
# self.assertEqual(choices_to_dict(response.data.get('ip-address:family')), )
self.assertEqual(choices_to_dict(response.data.get('ip-address:role')), IPAddressRoleChoices.as_dict())
self.assertEqual(choices_to_dict(response.data.get('ip-address:status')), IPAddressStatusChoices.as_dict())
# VLAN
self.assertEqual(choices_to_dict(response.data.get('vlan:status')), VLANStatusChoices.as_dict())
# Service
self.assertEqual(choices_to_dict(response.data.get('service:protocol')), ServiceProtocolChoices.as_dict())
class VRFTest(APITestCase):

View File

@ -14,9 +14,6 @@ class SecretsRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = SecretsRootView
# Field choices
router.register('_choices', views.SecretsFieldChoicesViewSet, basename='field-choice')
# Secrets
router.register('secret-roles', views.SecretRoleViewSet)
router.register('secrets', views.SecretViewSet)

View File

@ -11,7 +11,7 @@ from rest_framework.viewsets import ViewSet
from secrets import filters
from secrets.exceptions import InvalidKey
from secrets.models import Secret, SecretRole, SessionKey, UserKey
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.api import ModelViewSet
from . import serializers
ERR_USERKEY_MISSING = "No UserKey found for the current user."
@ -20,14 +20,6 @@ ERR_PRIVKEY_MISSING = "Private key was not provided."
ERR_PRIVKEY_INVALID = "Invalid private key."
#
# Field choices
#
class SecretsFieldChoicesViewSet(FieldChoicesViewSet):
fields = ()
#
# Secret Roles
#

View File

@ -19,13 +19,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('secrets-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
class SecretRoleTest(APITestCase):

View File

@ -14,9 +14,6 @@ class TenancyRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = TenancyRootView
# Field choices
router.register('_choices', views.TenancyFieldChoicesViewSet, basename='field-choice')
# Tenants
router.register('tenant-groups', views.TenantGroupViewSet)
router.register('tenants', views.TenantViewSet)

View File

@ -4,20 +4,12 @@ from extras.api.views import CustomFieldModelViewSet
from ipam.models import IPAddress, Prefix, VLAN, VRF
from tenancy import filters
from tenancy.models import Tenant, TenantGroup
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.api import ModelViewSet
from utilities.utils import get_subquery
from virtualization.models import VirtualMachine
from . import serializers
#
# Field choices
#
class TenancyFieldChoicesViewSet(FieldChoicesViewSet):
fields = ()
#
# Tenant Groups
#

View File

@ -14,13 +14,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('tenancy-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
class TenantGroupTest(APITestCase):

View File

@ -371,49 +371,3 @@ class ModelViewSet(_ModelViewSet):
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Deleting {instance} (PK: {instance.pk})")
return super().perform_destroy(instance)
class FieldChoicesViewSet(ViewSet):
"""
Expose the built-in numeric values which represent static choices for a model's field.
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Compile a dict of all fields in this view
self._fields = OrderedDict()
for serializer_class, field_list in self.fields:
for field_name in field_list:
model_name = serializer_class.Meta.model._meta.verbose_name
key = ':'.join([model_name.lower().replace(' ', '-'), field_name])
serializer = serializer_class()
choices = []
for k, v in serializer.get_fields()[field_name].choices.items():
if type(v) in [list, tuple]:
for k2, v2 in v:
choices.append({
'value': k2,
'label': v2,
})
else:
choices.append({
'value': k,
'label': v,
})
self._fields[key] = choices
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 "Field Choices"

View File

@ -36,33 +36,6 @@ def create_test_user(username='testuser', permissions=None):
return user
def choices_to_dict(choices_list):
"""
Convert a list of field choices to a dictionary suitable for direct comparison with a ChoiceSet. For example:
[
{
"value": "choice-1",
"label": "First Choice"
},
{
"value": "choice-2",
"label": "Second Choice"
}
]
Becomes:
{
"choice-1": "First Choice",
"choice-2": "Second Choice
}
"""
return {
choice['value']: choice['label'] for choice in choices_list
}
@contextmanager
def disable_warnings(logger_name):
"""

View File

@ -14,9 +14,6 @@ class VirtualizationRootView(routers.APIRootView):
router = routers.DefaultRouter()
router.APIRootView = VirtualizationRootView
# Field choices
router.register('_choices', views.VirtualizationFieldChoicesViewSet, basename='field-choice')
# Clusters
router.register('cluster-types', views.ClusterTypeViewSet)
router.register('cluster-groups', views.ClusterGroupViewSet)

View File

@ -2,24 +2,13 @@ from django.db.models import Count
from dcim.models import Device, Interface
from extras.api.views import CustomFieldModelViewSet
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.api import ModelViewSet
from utilities.utils import get_subquery
from virtualization import filters
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
from . import serializers
#
# Field choices
#
class VirtualizationFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.VirtualMachineSerializer, ['status']),
(serializers.InterfaceSerializer, ['type']),
)
#
# Clusters
#

View File

@ -5,7 +5,7 @@ from rest_framework import status
from dcim.choices import InterfaceModeChoices
from dcim.models import Interface
from ipam.models import IPAddress, VLAN
from utilities.testing import APITestCase, choices_to_dict, disable_warnings
from utilities.testing import APITestCase, disable_warnings
from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -19,19 +19,6 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
def test_choices(self):
url = reverse('virtualization-api:field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
# VirtualMachine
self.assertEqual(choices_to_dict(response.data.get('virtual-machine:status')), VirtualMachineStatusChoices.as_dict())
# Interface
self.assertEqual(choices_to_dict(response.data.get('interface:type')), VMInterfaceTypeChoices.as_dict())
class ClusterTypeTest(APITestCase):