소스 검색

Enable dictionary specification of related objects in API

Jeremy Stretch 6 년 전
부모
커밋
de7207de55
4개의 변경된 파일125개의 추가작업 그리고 16개의 파일을 삭제
  1. 31 0
      CHANGELOG.md
  2. 24 11
      docs/api/overview.md
  3. 38 5
      netbox/utilities/api.py
  4. 32 0
      netbox/utilities/utils.py

+ 31 - 0
CHANGELOG.md

@@ -85,6 +85,36 @@ REDIS = {
 }
 }
 ```
 ```
 
 
+### API Support for Specifying Related Objects by Attributes([#3077](https://github.com/digitalocean/netbox/issues/3077))
+
+Previously, referencing a related object in an API request required knowing the primary key (integer ID) of that object.
+For example, when creating a new device, its rack would be specified as an integer:
+
+```
+{
+    "name": "MyNewDevice",
+    "rack": 123,
+    ...
+}
+```
+
+The NetBox API now supports referencing related objects by a set of sufficiently unique attrbiutes:
+
+```
+{
+    "name": "MyNewDevice",
+    "rack": {
+        "site": {
+            "name": "Equinix DC6"
+        },
+        "name": "R204"
+    },
+    ...
+}
+```
+
+Note that if the provided parameters do not return exactly one object, a validation error is raised.
+
 ### API Device/VM Config Context Included by Default ([#2350](https://github.com/digitalocean/netbox/issues/2350))
 ### API Device/VM Config Context Included by Default ([#2350](https://github.com/digitalocean/netbox/issues/2350))
 
 
 The rendered Config Context for Devices and VMs is now included by default in all API results (list and detail views).
 The rendered Config Context for Devices and VMs is now included by default in all API results (list and detail views).
@@ -112,6 +142,7 @@ to now use "Extras | Tag."
 
 
 ## API Changes
 ## API Changes
 
 
+* ForeignKey fields now accept either the related object PK or a dictionary of attributes describing the related object.
 * dcim.Interface: `form_factor` has been renamed to `type`. Backward-compatibile support for `form_factor` will be maintained until NetBox v2.7.
 * dcim.Interface: `form_factor` has been renamed to `type`. Backward-compatibile support for `form_factor` will be maintained until NetBox v2.7.
 * dcim.Interface: The `type` filter has been renamed to `kind`.
 * dcim.Interface: The `type` filter has been renamed to `kind`.
 * dcim.DeviceType: `instance_count` has been renamed to `device_count`.
 * dcim.DeviceType: `instance_count` has been renamed to `device_count`.

+ 24 - 11
docs/api/overview.md

@@ -104,24 +104,37 @@ The base serializer is used to represent the default view of a model. This inclu
 }
 }
 ```
 ```
 
 
-Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
+## Related Objects
 
 
-When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
+Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
+
+For example, when creating a new device, its rack can be specified by NetBox ID (PK):
 
 
 ```
 ```
 {
 {
-    "id": 1201,
-    "site": 7,
-    "group": 4,
-    "vid": 102,
-    "name": "Users-Floor2",
-    "tenant": null,
-    "status": 1,
-    "role": 9,
-    "description": ""
+    "name": "MyNewDevice",
+    "rack": 123,
+    ...
 }
 }
 ```
 ```
 
 
+Or by a set of nested attributes used to identify the rack:
+
+```
+{
+    "name": "MyNewDevice",
+    "rack": {
+        "site": {
+            "name": "Equinix DC6"
+        },
+        "name": "R204"
+    },
+    ...
+}
+```
+
+Note that if the provided parameters do not return exactly one object, a validation error is raised.
+
 ## Brief Format
 ## Brief Format
 
 
 Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.
 Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.

+ 38 - 5
netbox/utilities/api.py

@@ -3,7 +3,7 @@ from collections import OrderedDict
 import pytz
 import pytz
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
 from django.db.models import ManyToManyField
 from django.db.models import ManyToManyField
 from django.http import Http404
 from django.http import Http404
 from django.utils.decorators import method_decorator
 from django.utils.decorators import method_decorator
@@ -15,7 +15,7 @@ from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
 
 
-from .utils import dynamic_import
+from .utils import dict_to_filter_params, dynamic_import
 
 
 
 
 class ServiceUnavailable(APIException):
 class ServiceUnavailable(APIException):
@@ -202,15 +202,48 @@ class WritableNestedSerializer(ModelSerializer):
     """
     """
     Returns a nested representation of an object on read, but accepts only a primary key on write.
     Returns a nested representation of an object on read, but accepts only a primary key on write.
     """
     """
+
     def to_internal_value(self, data):
     def to_internal_value(self, data):
+
         if data is None:
         if data is None:
             return None
             return None
+
+        # Dictionary of related object attributes
+        if isinstance(data, dict):
+            params = dict_to_filter_params(data)
+            try:
+                return self.Meta.model.objects.get(**params)
+            except ObjectDoesNotExist:
+                raise ValidationError(
+                    "Related object not found using the provided attributes: {}".format(params)
+                )
+            except MultipleObjectsReturned:
+                raise ValidationError(
+                    "Multiple objects match the provided attributes: {}".format(params)
+                )
+            except FieldError as e:
+                raise ValidationError(e)
+
+        # Integer PK of related object
+        if isinstance(data, int):
+            pk = data
+        else:
+            try:
+                # PK might have been mistakenly passed as a string
+                pk = int(data)
+            except (TypeError, ValueError):
+                raise ValidationError(
+                    "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
+                    "unrecognized value: {}".format(data)
+                )
+
+        # Look up object by PK
         try:
         try:
             return self.Meta.model.objects.get(pk=int(data))
             return self.Meta.model.objects.get(pk=int(data))
-        except (TypeError, ValueError):
-            raise ValidationError("Primary key must be an integer")
         except ObjectDoesNotExist:
         except ObjectDoesNotExist:
-            raise ValidationError("Invalid ID")
+            raise ValidationError(
+                "Related object not found using the provided numeric ID: {}".format(pk)
+            )
 
 
 
 
 #
 #

+ 32 - 0
netbox/utilities/utils.py

@@ -85,6 +85,38 @@ def serialize_object(obj, extra=None):
     return data
     return data
 
 
 
 
+def dict_to_filter_params(d, prefix=''):
+    """
+    Translate a dictionary of attributes to a nested set of parameters suitable for QuerySet filtering. For example:
+
+        {
+            "name": "Foo",
+            "rack": {
+                "facility_id": "R101"
+            }
+        }
+
+    Becomes:
+
+        {
+            "name": "Foo",
+            "rack__facility_id": "R101"
+        }
+
+    And can be employed as filter parameters:
+
+        Device.objects.filter(**dict_to_filter(attrs_dict))
+    """
+    params = {}
+    for key, val in d.items():
+        k = prefix + key
+        if isinstance(val, dict):
+            params.update(dict_to_filter_params(val, k + '__'))
+        else:
+            params[k] = val
+    return params
+
+
 def deepmerge(original, new):
 def deepmerge(original, new):
     """
     """
     Deep merge two dictionaries (new into original) and return a new dict
     Deep merge two dictionaries (new into original) and return a new dict