From ac44731684cc250a7c036a282ba675ddc71638d9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 20 Sep 2022 10:31:43 -0700 Subject: [PATCH] 10348 add decimal custom field --- netbox/extras/models/customfields.py | 16 +++++++++++- netbox/extras/tests/test_customfields.py | 32 +++++++++++++++++++----- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 170509ec4..5a7e21863 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -1,5 +1,6 @@ import re from datetime import datetime, date +import decimal import django_filters from django import forms @@ -318,7 +319,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge ) # Decimal - if self.type == CustomFieldTypeChoices.TYPE_DECIMAL: + elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL: field = forms.DecimalField( required=required, initial=initial, @@ -435,6 +436,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge elif self.type == CustomFieldTypeChoices.TYPE_INTEGER: filter_class = filters.MultiValueNumberFilter + # Decimal + elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL: + filter_class = filters.MultiValueNumberFilter + # Boolean elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: filter_class = django_filters.BooleanFilter @@ -492,6 +497,15 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge if self.validation_maximum is not None and value > self.validation_maximum: raise ValidationError(f"Value must not exceed {self.validation_maximum}") + # Validate decimal + if self.type == CustomFieldTypeChoices.TYPE_DECIMAL: + if type(value) is not decimal.Decimal: + raise ValidationError("Value must be a decimal.") + if self.validation_minimum is not None and value < self.validation_minimum: + raise ValidationError(f"Value must be at least {self.validation_minimum}") + if self.validation_maximum is not None and value > self.validation_maximum: + raise ValidationError(f"Value must not exceed {self.validation_maximum}") + # Validate boolean if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]: raise ValidationError("Value must be true or false.") diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 4aa63defc..435af89d9 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -432,6 +432,7 @@ class CustomFieldAPITest(APITestCase): object_type=ContentType.objects.get_for_model(VLAN), default=[vlans[0].pk, vlans[1].pk], ), + CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45), ) for cf in custom_fields: cf.save() @@ -458,6 +459,7 @@ class CustomFieldAPITest(APITestCase): custom_fields[8].name: ['Bar', 'Baz'], custom_fields[9].name: vlans[1].pk, custom_fields[10].name: [vlans[2].pk, vlans[3].pk], + custom_fields[11].name: 456.78, } sites[1].save() @@ -474,6 +476,7 @@ class CustomFieldAPITest(APITestCase): CustomFieldTypeChoices.TYPE_MULTISELECT: 'array', CustomFieldTypeChoices.TYPE_OBJECT: 'object', CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array', + CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal', } self.add_permissions('extras.view_customfield') @@ -508,6 +511,7 @@ class CustomFieldAPITest(APITestCase): 'multiselect_field': None, 'object_field': None, 'multiobject_field': None, + 'decimal_field': None, }) def test_get_single_object_with_custom_field_data(self): @@ -535,6 +539,7 @@ class CustomFieldAPITest(APITestCase): [obj['id'] for obj in response.data['custom_fields']['multiobject_field']], site2_cfvs['multiobject_field'] ) + self.assertEqual(response.data['custom_fields']['decimal_field'], site2_cfvs['decimal_field']) def test_create_single_object_with_defaults(self): """ @@ -569,6 +574,7 @@ class CustomFieldAPITest(APITestCase): [obj['id'] for obj in response.data['custom_fields']['multiobject_field']], cf_defaults['multiobject_field'] ) + self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field']) # Validate database data site = Site.objects.get(pk=response.data['id']) @@ -583,6 +589,7 @@ class CustomFieldAPITest(APITestCase): 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']) + self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field']) def test_create_single_object_with_values(self): """ @@ -603,6 +610,7 @@ class CustomFieldAPITest(APITestCase): '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)), + 'decimal_field': 456.78, }, } url = reverse('dcim-api:site-list') @@ -628,6 +636,7 @@ class CustomFieldAPITest(APITestCase): [obj['id'] for obj in response_cf['multiobject_field']], data_cf['multiobject_field'] ) + self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field']) # Validate database data site = Site.objects.get(pk=response.data['id']) @@ -642,6 +651,7 @@ class CustomFieldAPITest(APITestCase): 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']) + self.assertEqual(site.custom_field_data['decimal_field'], data_cf['decimal_field']) def test_create_multiple_objects_with_defaults(self): """ @@ -690,6 +700,7 @@ class CustomFieldAPITest(APITestCase): [obj['id'] for obj in response_cf['multiobject_field']], cf_defaults['multiobject_field'] ) + self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field']) # Validate database data site = Site.objects.get(pk=response.data[i]['id']) @@ -704,6 +715,7 @@ class CustomFieldAPITest(APITestCase): 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']) + self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field']) def test_create_multiple_objects_with_values(self): """ @@ -721,6 +733,7 @@ class CustomFieldAPITest(APITestCase): '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)), + 'decimal_field': 456.78, } data = ( { @@ -764,6 +777,7 @@ class CustomFieldAPITest(APITestCase): [obj['id'] for obj in response_cf['multiobject_field']], custom_field_data['multiobject_field'] ) + self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field']) # Validate database data site = Site.objects.get(pk=response.data[i]['id']) @@ -778,6 +792,7 @@ class CustomFieldAPITest(APITestCase): 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']) + self.assertEqual(site.custom_field_data['decimal_field'], custom_field_data['decimal_field']) def test_update_single_object_with_values(self): """ @@ -814,6 +829,7 @@ class CustomFieldAPITest(APITestCase): [obj['id'] for obj in response_cf['multiobject_field']], original_cfvs['multiobject_field'] ) + self.assertEqual(response_cf['decimal_field'], data['custom_fields']['decimal_field']) # Validate database data site2.refresh_from_db() @@ -828,6 +844,7 @@ class CustomFieldAPITest(APITestCase): 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']) + self.assertEqual(site2.custom_field_data['decimal_field'], data['custom_fields']['decimal_field']) def test_minimum_maximum_values_validation(self): site2 = Site.objects.get(name='Site 2') @@ -896,6 +913,7 @@ class CustomFieldImportTest(TestCase): CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=[ 'Choice A', 'Choice B', 'Choice C', ]), + CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL), ) for cf in custom_fields: cf.save() @@ -906,10 +924,10 @@ class CustomFieldImportTest(TestCase): Import a Site in CSV format, including a value for each CustomField. """ data = ( - ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'), - ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'), - ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'), - ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', ''), + ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect', 'cf_decimal'), + ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"', '123.45'), + ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"', '456.78'), + ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', ''), ) csv_data = '\n'.join(','.join(row) for row in data) @@ -919,7 +937,7 @@ class CustomFieldImportTest(TestCase): # Validate data for site 1 site1 = Site.objects.get(name='Site 1') - self.assertEqual(len(site1.custom_field_data), 9) + self.assertEqual(len(site1.custom_field_data), 10) self.assertEqual(site1.custom_field_data['text'], 'ABC') self.assertEqual(site1.custom_field_data['longtext'], 'Foo') self.assertEqual(site1.custom_field_data['integer'], 123) @@ -929,10 +947,11 @@ class CustomFieldImportTest(TestCase): self.assertEqual(site1.custom_field_data['json'], {"foo": 123}) self.assertEqual(site1.custom_field_data['select'], 'Choice A') self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B']) + self.assertEqual(site1.custom_field_data['decimal'], '123.45') # Validate data for site 2 site2 = Site.objects.get(name='Site 2') - self.assertEqual(len(site2.custom_field_data), 9) + self.assertEqual(len(site2.custom_field_data), 10) self.assertEqual(site2.custom_field_data['text'], 'DEF') self.assertEqual(site2.custom_field_data['longtext'], 'Bar') self.assertEqual(site2.custom_field_data['integer'], 456) @@ -942,6 +961,7 @@ class CustomFieldImportTest(TestCase): self.assertEqual(site2.custom_field_data['json'], {"bar": 456}) self.assertEqual(site2.custom_field_data['select'], 'Choice B') self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C']) + self.assertEqual(site2.custom_field_data['decimal'], '456.78') # No custom field data should be set for site 3 site3 = Site.objects.get(name='Site 3')