diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 3f436970d..5bb1f033d 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,6 +1,7 @@ from datetime import datetime from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -29,10 +30,17 @@ class CustomFieldDefaultValues: value = {} for field in fields: if field.default: - if field.type == CustomFieldTypeChoices.TYPE_SELECT: - field_value = field.choices.get(value=field.default).pk + if field.type == CustomFieldTypeChoices.TYPE_INTEGER: + field_value = int(field.default) elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN: - field_value = bool(field.default) + # TODO: Fix default value assignment for boolean custom fields + field_value = False if field.default.lower() == 'false' else bool(field.default) + elif field.type == CustomFieldTypeChoices.TYPE_SELECT: + try: + field_value = field.choices.get(value=field.default).pk + except ObjectDoesNotExist: + # Invalid default value + field_value = None else: field_value = field.default value[field.name] = field_value diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index a6e2bfcec..33ce9cca2 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -101,240 +101,274 @@ class CustomFieldTest(TestCase): class CustomFieldAPITest(APITestCase): - def setUp(self): - - super().setUp() + @classmethod + def setUpTestData(cls): content_type = ContentType.objects.get_for_model(Site) # Text custom field - self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='magic_word') - self.cf_text.save() - self.cf_text.obj_type.set([content_type]) - self.cf_text.save() + cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') + cls.cf_text.save() + cls.cf_text.obj_type.set([content_type]) # Integer custom field - self.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='magic_number') - self.cf_integer.save() - self.cf_integer.obj_type.set([content_type]) - self.cf_integer.save() + cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123) + cls.cf_integer.save() + cls.cf_integer.obj_type.set([content_type]) # Boolean custom field - self.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='is_magic') - self.cf_boolean.save() - self.cf_boolean.obj_type.set([content_type]) - self.cf_boolean.save() + cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False) + cls.cf_boolean.save() + cls.cf_boolean.obj_type.set([content_type]) # Date custom field - self.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='magic_date') - self.cf_date.save() - self.cf_date.obj_type.set([content_type]) - self.cf_date.save() + cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01') + cls.cf_date.save() + cls.cf_date.obj_type.set([content_type]) # URL custom field - self.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='magic_url') - self.cf_url.save() - self.cf_url.obj_type.set([content_type]) - self.cf_url.save() + cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1') + cls.cf_url.save() + cls.cf_url.obj_type.set([content_type]) # Select custom field - self.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='magic_choice') - self.cf_select.save() - self.cf_select.obj_type.set([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() + cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field') + cls.cf_select.save() + cls.cf_select.obj_type.set([content_type]) + cls.cf_select_choice1 = CustomFieldChoice(field=cls.cf_select, value='Foo') + cls.cf_select_choice1.save() + cls.cf_select_choice2 = CustomFieldChoice(field=cls.cf_select, value='Bar') + cls.cf_select_choice2.save() + cls.cf_select_choice3 = CustomFieldChoice(field=cls.cf_select, value='Baz') + cls.cf_select_choice3.save() - self.site = Site.objects.create(name='Test Site 1', slug='test-site-1') + cls.cf_select.default = cls.cf_select_choice1.value + cls.cf_select.save() - def test_get_obj_without_custom_fields(self): + # 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'], { + # 'text_field': None, + # 'number_field': None, + # 'boolean_field': None, + # 'date_field': None, + # 'url_field': None, + # 'choice_field': None, + # }) - url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk}) - response = self.client.get(url, **self.header) + # def test_get_single_object_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('text_field'), CUSTOM_FIELD_VALUES[0][1]) + # self.assertEqual(response.data['custom_fields'].get('number_field'), CUSTOM_FIELD_VALUES[1][1]) + # self.assertEqual(response.data['custom_fields'].get('boolean_field'), CUSTOM_FIELD_VALUES[2][1]) + # self.assertEqual(response.data['custom_fields'].get('date_field'), CUSTOM_FIELD_VALUES[3][1]) + # self.assertEqual(response.data['custom_fields'].get('url_field'), CUSTOM_FIELD_VALUES[4][1]) + # self.assertEqual(response.data['custom_fields'].get('choice_field'), { + # 'value': self.cf_select_choice1.pk, 'label': 'Foo' + # }) - 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': 0, - } - } - - 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': '2017-04-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.isoformat(), 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']) - - def test_set_custom_field_defaults(self): + def test_create_single_object_with_defaults(self): """ - Create a new object with no custom field data. Custom field values should be created using the custom fields' - default values. + Create a new site and check that it received the default custom field values. """ - CUSTOM_FIELD_DEFAULTS = { - 'magic_word': 'foobar', - 'magic_number': '123', - 'is_magic': 'true', - 'magic_date': '2019-12-13', - 'magic_url': 'http://example.com/', - 'magic_choice': self.cf_select_choice1.value, - } - - # Update CustomFields to set default values - for field_name, default_value in CUSTOM_FIELD_DEFAULTS.items(): - CustomField.objects.filter(name=field_name).update(default=default_value) - data = { - 'name': 'Test Site X', - 'slug': 'test-site-x', + 'name': 'Site 2', + 'slug': 'site-2', } url = reverse('dcim-api:site-list') response = self.client.post(url, data, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(response.data['custom_fields']['magic_word'], CUSTOM_FIELD_DEFAULTS['magic_word']) - self.assertEqual(response.data['custom_fields']['magic_number'], str(CUSTOM_FIELD_DEFAULTS['magic_number'])) - self.assertEqual(response.data['custom_fields']['is_magic'], bool(CUSTOM_FIELD_DEFAULTS['is_magic'])) - self.assertEqual(response.data['custom_fields']['magic_date'], CUSTOM_FIELD_DEFAULTS['magic_date']) - self.assertEqual(response.data['custom_fields']['magic_url'], CUSTOM_FIELD_DEFAULTS['magic_url']) - self.assertEqual(response.data['custom_fields']['magic_choice'], self.cf_select_choice1.pk) + + # Validate response data + response_cf = response.data['custom_fields'] + self.assertEqual(response_cf['text_field'], self.cf_text.default) + self.assertEqual(response_cf['number_field'], self.cf_integer.default) + self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default) + self.assertEqual(response_cf['date_field'], self.cf_date.default) + self.assertEqual(response_cf['url_field'], self.cf_url.default) + self.assertEqual(response_cf['choice_field'], self.cf_select_choice1.pk) + + # Validate database data + site = Site.objects.get(pk=response.data['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], self.cf_text.default) + self.assertEqual(cfvs['number_field'], self.cf_integer.default) + self.assertEqual(cfvs['boolean_field'], self.cf_boolean.default) + self.assertEqual(str(cfvs['date_field']), self.cf_date.default) + self.assertEqual(cfvs['url_field'], self.cf_url.default) + self.assertEqual(cfvs['choice_field'].pk, self.cf_select_choice1.pk) + + def test_create_single_object_with_values(self): + """ + Create a single new site with a value for each type of custom field. + """ + data = { + 'name': 'Site 2', + 'slug': 'site-2', + 'custom_fields': { + 'text_field': 'bar', + 'number_field': 456, + 'boolean_field': True, + 'date_field': '2020-01-02', + 'url_field': 'http://example.com/2', + 'choice_field': self.cf_select_choice2.pk, + }, + } + + url = reverse('dcim-api:site-list') + response = self.client.post(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_201_CREATED) + + # Validate response data + response_cf = response.data['custom_fields'] + data_cf = data['custom_fields'] + self.assertEqual(response_cf['text_field'], data_cf['text_field']) + self.assertEqual(response_cf['number_field'], data_cf['number_field']) + self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field']) + self.assertEqual(response_cf['date_field'], data_cf['date_field']) + self.assertEqual(response_cf['url_field'], data_cf['url_field']) + self.assertEqual(response_cf['choice_field'], data_cf['choice_field']) + + # Validate database data + site = Site.objects.get(pk=response.data['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], data_cf['text_field']) + self.assertEqual(cfvs['number_field'], data_cf['number_field']) + self.assertEqual(cfvs['boolean_field'], data_cf['boolean_field']) + self.assertEqual(str(cfvs['date_field']), data_cf['date_field']) + self.assertEqual(cfvs['url_field'], data_cf['url_field']) + self.assertEqual(cfvs['choice_field'].pk, data_cf['choice_field']) + + def test_create_multiple_objects_with_defaults(self): + """ + Create three news sites and check that each received the default custom field values. + """ + data = ( + { + 'name': 'Site 2', + 'slug': 'site-2', + }, + { + 'name': 'Site 3', + 'slug': 'site-3', + }, + { + 'name': 'Site 4', + 'slug': 'site-4', + }, + ) + + url = reverse('dcim-api:site-list') + response = self.client.post(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(len(response.data), len(data)) + + for i, obj in enumerate(data): + + # Validate response data + response_cf = response.data[i]['custom_fields'] + self.assertEqual(response_cf['text_field'], self.cf_text.default) + self.assertEqual(response_cf['number_field'], self.cf_integer.default) + self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default) + self.assertEqual(response_cf['date_field'], self.cf_date.default) + self.assertEqual(response_cf['url_field'], self.cf_url.default) + self.assertEqual(response_cf['choice_field'], self.cf_select_choice1.pk) + + # Validate database data + site = Site.objects.get(pk=response.data[i]['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], self.cf_text.default) + self.assertEqual(cfvs['number_field'], self.cf_integer.default) + self.assertEqual(cfvs['boolean_field'], self.cf_boolean.default) + self.assertEqual(str(cfvs['date_field']), self.cf_date.default) + self.assertEqual(cfvs['url_field'], self.cf_url.default) + self.assertEqual(cfvs['choice_field'].pk, self.cf_select_choice1.pk) + + def test_create_multiple_objects_with_values(self): + """ + Create a three new sites, each with custom fields defined. + """ + custom_field_data = { + 'text_field': 'bar', + 'number_field': 456, + 'boolean_field': True, + 'date_field': '2020-01-02', + 'url_field': 'http://example.com/2', + 'choice_field': self.cf_select_choice2.pk, + } + data = ( + { + 'name': 'Site 2', + 'slug': 'site-2', + 'custom_fields': custom_field_data, + }, + { + 'name': 'Site 3', + 'slug': 'site-3', + 'custom_fields': custom_field_data, + }, + { + 'name': 'Site 4', + 'slug': 'site-4', + 'custom_fields': custom_field_data, + }, + ) + + url = reverse('dcim-api:site-list') + response = self.client.post(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(len(response.data), len(data)) + + for i, obj in enumerate(data): + + # Validate response data + response_cf = response.data[i]['custom_fields'] + self.assertEqual(response_cf['text_field'], custom_field_data['text_field']) + self.assertEqual(response_cf['number_field'], custom_field_data['number_field']) + self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field']) + 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['choice_field'], custom_field_data['choice_field']) + + # Validate database data + site = Site.objects.get(pk=response.data[i]['id']) + cfvs = { + cfv.field.name: cfv.value for cfv in site.custom_field_values.all() + } + self.assertEqual(cfvs['text_field'], custom_field_data['text_field']) + self.assertEqual(cfvs['number_field'], custom_field_data['number_field']) + self.assertEqual(cfvs['boolean_field'], custom_field_data['boolean_field']) + self.assertEqual(str(cfvs['date_field']), custom_field_data['date_field']) + self.assertEqual(cfvs['url_field'], custom_field_data['url_field']) + self.assertEqual(cfvs['choice_field'].pk, custom_field_data['choice_field']) class CustomFieldChoiceAPITest(APITestCase):