From 10823e1c37abcb1276b67dab6214e847ed51818c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Apr 2017 13:00:28 -0400 Subject: [PATCH 1/4] Got rudimentary custom field creates/updates working --- netbox/dcim/api/serializers.py | 4 +- netbox/extras/api/customfields.py | 79 +++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index a3149f963..de93f836b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -66,13 +66,13 @@ class NestedSiteSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'slug'] -class WritableSiteSerializer(serializers.ModelSerializer): +class WritableSiteSerializer(CustomFieldModelSerializer): class Meta: model = Site fields = [ 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', - 'contact_name', 'contact_phone', 'contact_email', 'comments', + 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', ] diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index e47eb41ab..4b96cd00c 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,6 +1,8 @@ from django.contrib.contenttypes.models import ContentType +from django.db import transaction from rest_framework import serializers +from rest_framework.exceptions import ValidationError from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue @@ -14,6 +16,24 @@ class CustomFieldsSerializer(serializers.BaseSerializer): def to_representation(self, obj): return obj + def to_internal_value(self, data): + + parent_content_type = ContentType.objects.get_for_model(self.parent.Meta.model) + + for custom_field, value in data.items(): + + # Validate custom field name + try: + cf = CustomField.objects.get(name=custom_field) + except CustomField.DoesNotExist: + raise ValidationError(u"Unknown custom field: {}".format(custom_field)) + + # Validate custom field content type + if parent_content_type not in cf.obj_type.all(): + raise ValidationError(u"Invalid custom field for {} objects".format(parent_content_type)) + + return data + class CustomFieldModelSerializer(serializers.ModelSerializer): """ @@ -34,16 +54,57 @@ class CustomFieldModelSerializer(serializers.ModelSerializer): super(CustomFieldModelSerializer, self).__init__(*args, **kwargs) - # Retrieve the set of CustomFields which apply to this type of object - content_type = ContentType.objects.get_for_model(self.Meta.model) - fields = CustomField.objects.filter(obj_type=content_type) + if self.instance is not None: - # Populate CustomFieldValues for each instance from database - try: - for obj in self.instance: - _populate_custom_fields(obj, fields) - except TypeError: - _populate_custom_fields(self.instance, fields) + # Retrieve the set of CustomFields which apply to this type of object + content_type = ContentType.objects.get_for_model(self.Meta.model) + fields = CustomField.objects.filter(obj_type=content_type) + + # Populate CustomFieldValues for each instance from database + try: + for obj in self.instance: + _populate_custom_fields(obj, fields) + except TypeError: + _populate_custom_fields(self.instance, fields) + + def _save_custom_fields(self, instance, custom_fields): + content_type = ContentType.objects.get_for_model(self.Meta.model) + for field_name, value in custom_fields.items(): + custom_field = CustomField.objects.get(name=field_name) + CustomFieldValue.objects.update_or_create( + field=custom_field, + obj_type=content_type, + obj_id=instance.pk, + defaults={'serialized_value': value}, + ) + + def create(self, validated_data): + + custom_fields = validated_data.pop('custom_fields') + + with transaction.atomic(): + + instance = super(CustomFieldModelSerializer, self).create(validated_data) + + # Save custom fields + self._save_custom_fields(instance, custom_fields) + instance.custom_fields = custom_fields + + return instance + + def update(self, instance, validated_data): + + custom_fields = validated_data.pop('custom_fields') + + with transaction.atomic(): + + instance = super(CustomFieldModelSerializer, self).update(instance, validated_data) + + # Save custom fields + self._save_custom_fields(instance, custom_fields) + instance.custom_fields = custom_fields + + return instance class CustomFieldChoiceSerializer(serializers.ModelSerializer): From 1c86b00b5c7d09eef3c74c02434cf8a19822e05f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Apr 2017 14:53:18 -0400 Subject: [PATCH 2/4] Added custom field API tests --- netbox/extras/api/customfields.py | 6 + netbox/extras/models.py | 5 +- netbox/extras/tests/test_customfields.py | 215 ++++++++++++++++++++++- 3 files changed, 221 insertions(+), 5 deletions(-) diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 4b96cd00c..117c6a5fd 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -32,6 +32,12 @@ class CustomFieldsSerializer(serializers.BaseSerializer): if parent_content_type not in cf.obj_type.all(): raise ValidationError(u"Invalid custom field for {} objects".format(parent_content_type)) + # Validate selected choice + if cf.type == CF_TYPE_SELECT: + valid_choices = [c.pk for c in cf.choices.all()] + if value not in valid_choices: + raise ValidationError(u"Invalid choice ({}) for field {}".format(value, custom_field)) + return data diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 5d90613b0..485448336 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -156,10 +156,7 @@ class CustomField(models.Model): # Read date as YYYY-MM-DD return date(*[int(n) for n in serialized_value.split('-')]) if self.type == CF_TYPE_SELECT: - try: - return self.choices.get(pk=int(serialized_value)) - except CustomFieldChoice.DoesNotExist: - return None + return self.choices.get(pk=int(serialized_value)) return serialized_value diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 791c6a1a2..7986431bf 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,7 +1,12 @@ from datetime import date +from rest_framework import status +from rest_framework.test import APITestCase + +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from django.urls import reverse from dcim.models import Site @@ -9,9 +14,11 @@ from extras.models import ( CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, ) +from users.models import Token +from utilities.tests import HttpStatusMixin -class CustomFieldTestCase(TestCase): +class CustomFieldTest(TestCase): def setUp(self): @@ -95,3 +102,209 @@ class CustomFieldTestCase(TestCase): # Delete the custom field cf.delete() + + +class CustomFieldAPITest(HttpStatusMixin, APITestCase): + + def setUp(self): + + user = User.objects.create(username='testuser', is_superuser=True) + token = Token.objects.create(user=user) + self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)} + + content_type = ContentType.objects.get_for_model(Site) + + # Text custom field + self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word') + self.cf_text.save() + self.cf_text.obj_type = [content_type] + self.cf_text.save() + + # Integer custom field + self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number') + self.cf_integer.save() + self.cf_integer.obj_type = [content_type] + self.cf_integer.save() + + # Boolean custom field + self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic') + self.cf_boolean.save() + self.cf_boolean.obj_type = [content_type] + self.cf_boolean.save() + + # Date custom field + self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date') + self.cf_date.save() + self.cf_date.obj_type = [content_type] + self.cf_date.save() + + # URL custom field + self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url') + self.cf_url.save() + self.cf_url.obj_type = [content_type] + self.cf_url.save() + + # Select custom field + self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice') + self.cf_select.save() + self.cf_select.obj_type = [content_type] + self.cf_select.save() + self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo') + self.cf_select_choice1.save() + self.cf_select_choice2 = CustomFieldChoice(field=self.cf_select, value='Bar') + self.cf_select_choice2.save() + self.cf_select_choice3 = CustomFieldChoice(field=self.cf_select, value='Baz') + self.cf_select_choice3.save() + + self.site = Site.objects.create(name='Test Site 1', slug='test-site-1') + + def test_get_obj_without_custom_fields(self): + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.site.name) + self.assertEqual(response.data['custom_fields'], { + 'magic_word': None, + 'magic_number': None, + 'is_magic': None, + 'magic_date': None, + 'magic_url': None, + 'magic_choice': None, + }) + + def test_get_obj_with_custom_fields(self): + + CUSTOM_FIELD_VALUES = [ + (self.cf_text, 'Test string'), + (self.cf_integer, 1234), + (self.cf_boolean, True), + (self.cf_date, date(2016, 6, 23)), + (self.cf_url, 'http://example.com/'), + (self.cf_select, self.cf_select_choice1.pk), + ] + for field, value in CUSTOM_FIELD_VALUES: + cfv = CustomFieldValue(field=field, obj=self.site) + cfv.value = value + cfv.save() + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.site.name) + self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1]) + self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1]) + self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1]) + self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1]) + self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1]) + self.assertEqual(response.data['custom_fields'].get('magic_choice'), { + 'value': self.cf_select_choice1.pk, 'label': 'Foo' + }) + + def test_set_custom_field_text(self): + + data = { + 'name': 'Test Site 1', + 'slug': 'test-site-1', + 'custom_fields': { + 'magic_word': 'Foo bar baz', + } + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['custom_fields'].get('magic_word'), data['custom_fields']['magic_word']) + cfv = self.site.custom_field_values.get(field=self.cf_text) + self.assertEqual(cfv.value, data['custom_fields']['magic_word']) + + def test_set_custom_field_integer(self): + + data = { + 'name': 'Test Site 1', + 'slug': 'test-site-1', + 'custom_fields': { + 'magic_number': 42, + } + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['custom_fields'].get('magic_number'), data['custom_fields']['magic_number']) + cfv = self.site.custom_field_values.get(field=self.cf_integer) + self.assertEqual(cfv.value, data['custom_fields']['magic_number']) + + def test_set_custom_field_boolean(self): + + data = { + 'name': 'Test Site 1', + 'slug': 'test-site-1', + 'custom_fields': { + 'is_magic': False, + } + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['custom_fields'].get('is_magic'), data['custom_fields']['is_magic']) + cfv = self.site.custom_field_values.get(field=self.cf_boolean) + self.assertEqual(cfv.value, data['custom_fields']['is_magic']) + + def test_set_custom_field_date(self): + + data = { + 'name': 'Test Site 1', + 'slug': 'test-site-1', + 'custom_fields': { + 'magic_date': date(2017, 4, 25), + } + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['custom_fields'].get('magic_date'), data['custom_fields']['magic_date']) + cfv = self.site.custom_field_values.get(field=self.cf_date) + self.assertEqual(cfv.value, data['custom_fields']['magic_date']) + + def test_set_custom_field_url(self): + + data = { + 'name': 'Test Site 1', + 'slug': 'test-site-1', + 'custom_fields': { + 'magic_url': 'http://example.com/2/', + } + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['custom_fields'].get('magic_url'), data['custom_fields']['magic_url']) + cfv = self.site.custom_field_values.get(field=self.cf_url) + self.assertEqual(cfv.value, data['custom_fields']['magic_url']) + + def test_set_custom_field_select(self): + + data = { + 'name': 'Test Site 1', + 'slug': 'test-site-1', + 'custom_fields': { + 'magic_choice': self.cf_select_choice2.pk, + } + } + + url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice']) + cfv = self.site.custom_field_values.get(field=self.cf_select) + self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice']) From 117da337c79c0da9e7c1272c7c2889acf6e6c53d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 27 Apr 2017 12:46:04 -0400 Subject: [PATCH 3/4] Corrected tests and improved validation --- netbox/extras/api/customfields.py | 42 ++++++++++++++---------- netbox/extras/tests/test_customfields.py | 6 ++-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 117c6a5fd..dafed750b 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -18,25 +18,29 @@ class CustomFieldsSerializer(serializers.BaseSerializer): def to_internal_value(self, data): - parent_content_type = ContentType.objects.get_for_model(self.parent.Meta.model) + content_type = ContentType.objects.get_for_model(self.parent.Meta.model) + custom_fields = {field.name: field for field in CustomField.objects.filter(obj_type=content_type)} - for custom_field, value in data.items(): + for field_name, value in data.items(): # Validate custom field name - try: - cf = CustomField.objects.get(name=custom_field) - except CustomField.DoesNotExist: - raise ValidationError(u"Unknown custom field: {}".format(custom_field)) - - # Validate custom field content type - if parent_content_type not in cf.obj_type.all(): - raise ValidationError(u"Invalid custom field for {} objects".format(parent_content_type)) + if field_name not in custom_fields: + raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name)) # Validate selected choice + cf = custom_fields[field_name] if cf.type == CF_TYPE_SELECT: valid_choices = [c.pk for c in cf.choices.all()] if value not in valid_choices: - raise ValidationError(u"Invalid choice ({}) for field {}".format(value, custom_field)) + raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name)) + + # Check for missing required fields + missing_fields = [] + for field_name, field in custom_fields.items(): + if field.required and field_name not in data: + missing_fields.append(field_name) + if missing_fields: + raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields))) return data @@ -45,7 +49,7 @@ class CustomFieldModelSerializer(serializers.ModelSerializer): """ Extends ModelSerializer to render any CustomFields and their values associated with an object. """ - custom_fields = CustomFieldsSerializer() + custom_fields = CustomFieldsSerializer(required=False) def __init__(self, *args, **kwargs): @@ -86,29 +90,31 @@ class CustomFieldModelSerializer(serializers.ModelSerializer): def create(self, validated_data): - custom_fields = validated_data.pop('custom_fields') + custom_fields = validated_data.pop('custom_fields', None) with transaction.atomic(): instance = super(CustomFieldModelSerializer, self).create(validated_data) # Save custom fields - self._save_custom_fields(instance, custom_fields) - instance.custom_fields = custom_fields + if custom_fields is not None: + self._save_custom_fields(instance, custom_fields) + instance.custom_fields = custom_fields return instance def update(self, instance, validated_data): - custom_fields = validated_data.pop('custom_fields') + custom_fields = validated_data.pop('custom_fields', None) with transaction.atomic(): instance = super(CustomFieldModelSerializer, self).update(instance, validated_data) # Save custom fields - self._save_custom_fields(instance, custom_fields) - instance.custom_fields = custom_fields + if custom_fields is not None: + self._save_custom_fields(instance, custom_fields) + instance.custom_fields = custom_fields return instance diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 7986431bf..9e475fde8 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -243,7 +243,7 @@ class CustomFieldAPITest(HttpStatusMixin, APITestCase): 'name': 'Test Site 1', 'slug': 'test-site-1', 'custom_fields': { - 'is_magic': False, + 'is_magic': 0, } } @@ -261,7 +261,7 @@ class CustomFieldAPITest(HttpStatusMixin, APITestCase): 'name': 'Test Site 1', 'slug': 'test-site-1', 'custom_fields': { - 'magic_date': date(2017, 4, 25), + 'magic_date': '2017-04-25', } } @@ -271,7 +271,7 @@ class CustomFieldAPITest(HttpStatusMixin, APITestCase): self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['custom_fields'].get('magic_date'), data['custom_fields']['magic_date']) cfv = self.site.custom_field_values.get(field=self.cf_date) - self.assertEqual(cfv.value, data['custom_fields']['magic_date']) + self.assertEqual(cfv.value.isoformat(), data['custom_fields']['magic_date']) def test_set_custom_field_url(self): From c174c0cc6d9a81ee912b73cd70c7e90043f23a69 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 27 Apr 2017 12:50:43 -0400 Subject: [PATCH 4/4] Converted all necessary serializers to CustomFieldModelSerializers --- netbox/circuits/api/serializers.py | 14 ++++++++++---- netbox/dcim/api/serializers.py | 12 ++++++------ netbox/ipam/api/serializers.py | 23 +++++++++++++---------- netbox/tenancy/api/serializers.py | 4 ++-- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 4703458a6..b36d22105 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -28,11 +28,14 @@ class NestedProviderSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'slug'] -class WritableProviderSerializer(serializers.ModelSerializer): +class WritableProviderSerializer(CustomFieldModelSerializer): class Meta: model = Provider - fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] + fields = [ + 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + 'custom_fields', + ] # @@ -79,11 +82,14 @@ class NestedCircuitSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'cid'] -class WritableCircuitSerializer(serializers.ModelSerializer): +class WritableCircuitSerializer(CustomFieldModelSerializer): class Meta: model = Circuit - fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments'] + fields = [ + 'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', + 'custom_fields', + ] # diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index de93f836b..a9c52e3fd 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -150,13 +150,13 @@ class NestedRackSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'display_name'] -class WritableRackSerializer(serializers.ModelSerializer): +class WritableRackSerializer(CustomFieldModelSerializer): class Meta: model = Rack fields = [ 'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units', - 'comments', + 'comments', 'custom_fields', ] # Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This # prevents facility_id from being interpreted as a required field. @@ -263,13 +263,13 @@ class NestedDeviceTypeSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'manufacturer', 'model', 'slug'] -class WritableDeviceTypeSerializer(serializers.ModelSerializer): +class WritableDeviceTypeSerializer(CustomFieldModelSerializer): class Meta: model = DeviceType fields = [ 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', - 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', + 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields', ] @@ -476,13 +476,13 @@ class DeviceSerializer(CustomFieldModelSerializer): } -class WritableDeviceSerializer(serializers.ModelSerializer): +class WritableDeviceSerializer(CustomFieldModelSerializer): class Meta: model = Device fields = [ 'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack', - 'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', + 'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', 'custom_fields', ] validators = [] diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 5a350acb7..7d9a5778c 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -31,11 +31,11 @@ class NestedVRFSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'rd'] -class WritableVRFSerializer(serializers.ModelSerializer): +class WritableVRFSerializer(CustomFieldModelSerializer): class Meta: model = VRF - fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description'] + fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields'] # @@ -96,11 +96,11 @@ class NestedAggregateSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'family', 'prefix'] -class WritableAggregateSerializer(serializers.ModelSerializer): +class WritableAggregateSerializer(CustomFieldModelSerializer): class Meta: model = Aggregate - fields = ['id', 'prefix', 'rir', 'date_added', 'description'] + fields = ['id', 'prefix', 'rir', 'date_added', 'description', 'custom_fields'] # @@ -169,11 +169,11 @@ class NestedVLANSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'vid', 'name', 'display_name'] -class WritableVLANSerializer(serializers.ModelSerializer): +class WritableVLANSerializer(CustomFieldModelSerializer): class Meta: model = VLAN - fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description'] + fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'custom_fields'] validators = [] def validate(self, data): @@ -216,11 +216,14 @@ class NestedPrefixSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'family', 'prefix'] -class WritablePrefixSerializer(serializers.ModelSerializer): +class WritablePrefixSerializer(CustomFieldModelSerializer): class Meta: model = Prefix - fields = ['id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description'] + fields = [ + 'id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + 'custom_fields', + ] # @@ -252,11 +255,11 @@ IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer() IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer() -class WritableIPAddressSerializer(serializers.ModelSerializer): +class WritableIPAddressSerializer(CustomFieldModelSerializer): class Meta: model = IPAddress - fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside'] + fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields'] # diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 67231fe67..e649b6f03 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -43,8 +43,8 @@ class NestedTenantSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'slug'] -class WritableTenantSerializer(serializers.ModelSerializer): +class WritableTenantSerializer(CustomFieldModelSerializer): class Meta: model = Tenant - fields = ['id', 'name', 'slug', 'group', 'description', 'comments'] + fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields']