diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 5f8856a69..b00577bf8 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -12,6 +12,7 @@ * [#9223](https://github.com/netbox-community/netbox/issues/9223) - Fix serialization of array field values in change log * [#9878](https://github.com/netbox-community/netbox/issues/9878) - Fix spurious error message when rendering REST API docs * [#10236](https://github.com/netbox-community/netbox/issues/10236) - Fix TypeError exception when viewing PDU configured for three-phase power +* [#10241](https://github.com/netbox-community/netbox/issues/10241) - Support referencing custom field related objects by attribute in addition to PK * [#10579](https://github.com/netbox-community/netbox/issues/10579) - Mark cable traces terminating to a provider network as complete * [#10721](https://github.com/netbox-community/netbox/issues/10721) - Disable ordering by custom object field columns * [#10938](https://github.com/netbox-community/netbox/issues/10938) - `render_field` template tag should respect `label` kwarg diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index cb35b4e73..17e6f77c5 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -5,6 +5,7 @@ from rest_framework.serializers import ValidationError from extras.choices import CustomFieldTypeChoices from extras.models import CustomField from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model # @@ -69,6 +70,23 @@ class CustomFieldsDataField(Field): "values." ) + # Serialize object and multi-object values + for cf in self._get_custom_fields(): + if cf.name in data and cf.type in ( + CustomFieldTypeChoices.TYPE_OBJECT, + CustomFieldTypeChoices.TYPE_MULTIOBJECT + ): + serializer_class = get_serializer_for_model( + model=cf.object_type.model_class(), + prefix=NESTED_SERIALIZER_PREFIX + ) + many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT + serializer = serializer_class(data=data[cf.name], many=many, context=self.parent.context) + if serializer.is_valid(): + data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id'] + else: + raise ValidationError(f"Unknown related object(s): {data[cf.name]}") + # If updating an existing instance, start with existing custom_field_data if self.parent.instance: data = {**self.parent.instance.custom_field_data, **data} diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 946999bc2..a023dd7fb 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -803,6 +803,57 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(site2.custom_field_data['object_field'], original_cfvs['object_field']) self.assertEqual(site2.custom_field_data['multiobject_field'], original_cfvs['multiobject_field']) + def test_specify_related_object_by_attr(self): + site1 = Site.objects.get(name='Site 1') + vlans = VLAN.objects.all()[:3] + url = reverse('dcim-api:site-detail', kwargs={'pk': site1.pk}) + self.add_permissions('dcim.change_site') + + # Set related objects by PK + data = { + 'custom_fields': { + 'object_field': vlans[0].pk, + 'multiobject_field': [vlans[1].pk, vlans[2].pk], + }, + } + response = self.client.patch(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual( + response.data['custom_fields']['object_field']['id'], + vlans[0].pk + ) + self.assertListEqual( + [obj['id'] for obj in response.data['custom_fields']['multiobject_field']], + [vlans[1].pk, vlans[2].pk] + ) + + # Set related objects by name + data = { + 'custom_fields': { + 'object_field': { + 'name': vlans[0].name, + }, + 'multiobject_field': [ + { + 'name': vlans[1].name + }, + { + 'name': vlans[2].name + }, + ], + }, + } + response = self.client.patch(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual( + response.data['custom_fields']['object_field']['id'], + vlans[0].pk + ) + self.assertListEqual( + [obj['id'] for obj in response.data['custom_fields']['multiobject_field']], + [vlans[1].pk, vlans[2].pk] + ) + def test_minimum_maximum_values_validation(self): site2 = Site.objects.get(name='Site 2') url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})