diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index dec5843dc..09701f800 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -71,6 +71,7 @@ Inventory item templates can be arranged hierarchically within a device type, an * [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group * [#8295](https://github.com/netbox-community/netbox/issues/8295) - Webhook URLs can now be templatized * [#8296](https://github.com/netbox-community/netbox/issues/8296) - Allow disabling custom links +* [#8307](https://github.com/netbox-community/netbox/issues/8307) - Add `data_type` indicator to REST API serializer for custom fields ### Other Changes diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 79fab4a90..36b307b39 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -79,15 +79,28 @@ class CustomFieldSerializer(ValidatedModelSerializer): ) type = ChoiceField(choices=CustomFieldTypeChoices) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) + data_type = serializers.SerializerMethodField() class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic', - 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', - 'last_updated', + 'id', 'url', 'display', 'content_types', 'type', 'data_type', 'name', 'label', 'description', 'required', + 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', + 'choices', 'created', 'last_updated', ] + def get_data_type(self, obj): + types = CustomFieldTypeChoices + if obj.type == types.TYPE_INTEGER: + return 'integer' + if obj.type == types.TYPE_BOOLEAN: + return 'boolean' + if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT): + return 'object' + if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT): + return 'array' + return 'string' + # # Custom links diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 1a1fc13a8..3a5fe3ac9 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -378,9 +378,22 @@ class CustomFieldAPITest(APITestCase): CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01'), CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1'), CustomField(type=CustomFieldTypeChoices.TYPE_JSON, name='json_field', default='{"x": "y"}'), - CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field', default='Foo', choices=( - 'Foo', 'Bar', 'Baz' - )), + CustomField( + type=CustomFieldTypeChoices.TYPE_SELECT, + name='select_field', + default='Foo', + choices=( + 'Foo', 'Bar', 'Baz' + ) + ), + CustomField( + type=CustomFieldTypeChoices.TYPE_MULTISELECT, + name='multiselect_field', + default=['Foo'], + choices=( + 'Foo', 'Bar', 'Baz' + ) + ), CustomField( type=CustomFieldTypeChoices.TYPE_OBJECT, name='object_field', @@ -416,11 +429,37 @@ class CustomFieldAPITest(APITestCase): custom_fields[5].name: 'http://example.com/2', custom_fields[6].name: '{"foo": 1, "bar": 2}', custom_fields[7].name: 'Bar', - custom_fields[8].name: vlans[1].pk, - custom_fields[9].name: [vlans[2].pk, vlans[3].pk], + custom_fields[8].name: ['Bar', 'Baz'], + custom_fields[9].name: vlans[1].pk, + custom_fields[10].name: [vlans[2].pk, vlans[3].pk], } sites[1].save() + def test_get_custom_fields(self): + TYPES = { + CustomFieldTypeChoices.TYPE_TEXT: 'string', + CustomFieldTypeChoices.TYPE_LONGTEXT: 'string', + CustomFieldTypeChoices.TYPE_INTEGER: 'integer', + CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean', + CustomFieldTypeChoices.TYPE_DATE: 'string', + CustomFieldTypeChoices.TYPE_URL: 'string', + CustomFieldTypeChoices.TYPE_JSON: 'object', + CustomFieldTypeChoices.TYPE_SELECT: 'string', + CustomFieldTypeChoices.TYPE_MULTISELECT: 'array', + CustomFieldTypeChoices.TYPE_OBJECT: 'object', + CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array', + } + + self.add_permissions('extras.view_customfield') + url = reverse('extras-api:customfield-list') + response = self.client.get(url, **self.header) + self.assertEqual(response.data['count'], len(TYPES)) + + # Validate data types + for customfield in response.data['results']: + cf_type = customfield['type']['value'] + self.assertEqual(customfield['data_type'], TYPES[cf_type]) + def test_get_single_object_without_custom_field_data(self): """ Validate that custom fields are present on an object even if it has no values defined. @@ -439,7 +478,8 @@ class CustomFieldAPITest(APITestCase): 'date_field': None, 'url_field': None, 'json_field': None, - 'choice_field': None, + 'select_field': None, + 'multiselect_field': None, 'object_field': None, 'multiobject_field': None, }) @@ -462,7 +502,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field']) self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field']) self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field']) - self.assertEqual(response.data['custom_fields']['choice_field'], site2_cfvs['choice_field']) + self.assertEqual(response.data['custom_fields']['select_field'], site2_cfvs['select_field']) + self.assertEqual(response.data['custom_fields']['multiselect_field'], site2_cfvs['multiselect_field']) self.assertEqual(response.data['custom_fields']['object_field']['id'], site2_cfvs['object_field']) self.assertEqual( [obj['id'] for obj in response.data['custom_fields']['multiobject_field']], @@ -495,7 +536,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response_cf['date_field'], cf_defaults['date_field']) self.assertEqual(response_cf['url_field'], cf_defaults['url_field']) self.assertEqual(response_cf['json_field'], cf_defaults['json_field']) - self.assertEqual(response_cf['choice_field'], cf_defaults['choice_field']) + self.assertEqual(response_cf['select_field'], cf_defaults['select_field']) + self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field']) self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field']) self.assertEqual( [obj['id'] for obj in response.data['custom_fields']['multiobject_field']], @@ -511,7 +553,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field']) self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field']) self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field']) - self.assertEqual(site.custom_field_data['choice_field'], cf_defaults['choice_field']) + self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field']) + self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field']) self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field']) self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field']) @@ -530,7 +573,8 @@ class CustomFieldAPITest(APITestCase): 'date_field': '2020-01-02', 'url_field': 'http://example.com/2', 'json_field': '{"foo": 1, "bar": 2}', - 'choice_field': 'Bar', + 'select_field': 'Bar', + 'multiselect_field': ['Bar', 'Baz'], 'object_field': VLAN.objects.get(vid=2).pk, 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)), }, @@ -551,7 +595,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response_cf['date_field'], data_cf['date_field']) self.assertEqual(response_cf['url_field'], data_cf['url_field']) self.assertEqual(response_cf['json_field'], data_cf['json_field']) - self.assertEqual(response_cf['choice_field'], data_cf['choice_field']) + self.assertEqual(response_cf['select_field'], data_cf['select_field']) + self.assertEqual(response_cf['multiselect_field'], data_cf['multiselect_field']) self.assertEqual(response_cf['object_field']['id'], data_cf['object_field']) self.assertEqual( [obj['id'] for obj in response_cf['multiobject_field']], @@ -567,7 +612,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field']) self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field']) self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field']) - self.assertEqual(site.custom_field_data['choice_field'], data_cf['choice_field']) + self.assertEqual(site.custom_field_data['select_field'], data_cf['select_field']) + self.assertEqual(site.custom_field_data['multiselect_field'], data_cf['multiselect_field']) self.assertEqual(site.custom_field_data['object_field'], data_cf['object_field']) self.assertEqual(site.custom_field_data['multiobject_field'], data_cf['multiobject_field']) @@ -611,7 +657,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response_cf['date_field'], cf_defaults['date_field']) self.assertEqual(response_cf['url_field'], cf_defaults['url_field']) self.assertEqual(response_cf['json_field'], cf_defaults['json_field']) - self.assertEqual(response_cf['choice_field'], cf_defaults['choice_field']) + self.assertEqual(response_cf['select_field'], cf_defaults['select_field']) + self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field']) self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field']) self.assertEqual( [obj['id'] for obj in response_cf['multiobject_field']], @@ -627,7 +674,8 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field']) self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field']) self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field']) - self.assertEqual(site.custom_field_data['choice_field'], cf_defaults['choice_field']) + self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field']) + self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field']) self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field']) self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field']) @@ -643,7 +691,8 @@ class CustomFieldAPITest(APITestCase): 'date_field': '2020-01-02', 'url_field': 'http://example.com/2', 'json_field': '{"foo": 1, "bar": 2}', - 'choice_field': 'Bar', + 'select_field': 'Bar', + 'multiselect_field': ['Bar', 'Baz'], 'object_field': VLAN.objects.get(vid=2).pk, 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)), } @@ -682,7 +731,9 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response_cf['date_field'], custom_field_data['date_field']) self.assertEqual(response_cf['url_field'], custom_field_data['url_field']) self.assertEqual(response_cf['json_field'], custom_field_data['json_field']) - self.assertEqual(response_cf['choice_field'], custom_field_data['choice_field']) + self.assertEqual(response_cf['select_field'], custom_field_data['select_field']) + self.assertEqual(response_cf['multiselect_field'], custom_field_data['multiselect_field']) + self.assertEqual(response_cf['object_field']['id'], custom_field_data['object_field']) self.assertEqual( [obj['id'] for obj in response_cf['multiobject_field']], custom_field_data['multiobject_field'] @@ -697,7 +748,9 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field']) self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field']) self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field']) - self.assertEqual(site.custom_field_data['choice_field'], custom_field_data['choice_field']) + self.assertEqual(site.custom_field_data['select_field'], custom_field_data['select_field']) + self.assertEqual(site.custom_field_data['multiselect_field'], custom_field_data['multiselect_field']) + self.assertEqual(site.custom_field_data['object_field'], custom_field_data['object_field']) self.assertEqual(site.custom_field_data['multiobject_field'], custom_field_data['multiobject_field']) def test_update_single_object_with_values(self): @@ -728,7 +781,9 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response_cf['date_field'], original_cfvs['date_field']) self.assertEqual(response_cf['url_field'], original_cfvs['url_field']) self.assertEqual(response_cf['json_field'], original_cfvs['json_field']) - self.assertEqual(response_cf['choice_field'], original_cfvs['choice_field']) + self.assertEqual(response_cf['select_field'], original_cfvs['select_field']) + self.assertEqual(response_cf['multiselect_field'], original_cfvs['multiselect_field']) + self.assertEqual(response_cf['object_field']['id'], original_cfvs['object_field']) self.assertEqual( [obj['id'] for obj in response_cf['multiobject_field']], original_cfvs['multiobject_field'] @@ -743,7 +798,9 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(site2.custom_field_data['date_field'], original_cfvs['date_field']) self.assertEqual(site2.custom_field_data['url_field'], original_cfvs['url_field']) self.assertEqual(site2.custom_field_data['json_field'], original_cfvs['json_field']) - self.assertEqual(site2.custom_field_data['choice_field'], original_cfvs['choice_field']) + self.assertEqual(site2.custom_field_data['select_field'], original_cfvs['select_field']) + self.assertEqual(site2.custom_field_data['multiselect_field'], original_cfvs['multiselect_field']) + 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_minimum_maximum_values_validation(self):