mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-24 08:25:17 -06:00
10348 add decimal custom field
This commit is contained in:
parent
6bb170f9c4
commit
ac44731684
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
import decimal
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -318,7 +319,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Decimal
|
# Decimal
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
field = forms.DecimalField(
|
field = forms.DecimalField(
|
||||||
required=required,
|
required=required,
|
||||||
initial=initial,
|
initial=initial,
|
||||||
@ -435,6 +436,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
filter_class = filters.MultiValueNumberFilter
|
filter_class = filters.MultiValueNumberFilter
|
||||||
|
|
||||||
|
# Decimal
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
|
filter_class = filters.MultiValueNumberFilter
|
||||||
|
|
||||||
# Boolean
|
# Boolean
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
filter_class = django_filters.BooleanFilter
|
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:
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
raise ValidationError(f"Value must not exceed {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
|
# Validate boolean
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||||
raise ValidationError("Value must be true or false.")
|
raise ValidationError("Value must be true or false.")
|
||||||
|
@ -432,6 +432,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
object_type=ContentType.objects.get_for_model(VLAN),
|
object_type=ContentType.objects.get_for_model(VLAN),
|
||||||
default=[vlans[0].pk, vlans[1].pk],
|
default=[vlans[0].pk, vlans[1].pk],
|
||||||
),
|
),
|
||||||
|
CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45),
|
||||||
)
|
)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
cf.save()
|
cf.save()
|
||||||
@ -458,6 +459,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
custom_fields[8].name: ['Bar', 'Baz'],
|
custom_fields[8].name: ['Bar', 'Baz'],
|
||||||
custom_fields[9].name: vlans[1].pk,
|
custom_fields[9].name: vlans[1].pk,
|
||||||
custom_fields[10].name: [vlans[2].pk, vlans[3].pk],
|
custom_fields[10].name: [vlans[2].pk, vlans[3].pk],
|
||||||
|
custom_fields[11].name: 456.78,
|
||||||
}
|
}
|
||||||
sites[1].save()
|
sites[1].save()
|
||||||
|
|
||||||
@ -474,6 +476,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
CustomFieldTypeChoices.TYPE_MULTISELECT: 'array',
|
CustomFieldTypeChoices.TYPE_MULTISELECT: 'array',
|
||||||
CustomFieldTypeChoices.TYPE_OBJECT: 'object',
|
CustomFieldTypeChoices.TYPE_OBJECT: 'object',
|
||||||
CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array',
|
CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array',
|
||||||
|
CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal',
|
||||||
}
|
}
|
||||||
|
|
||||||
self.add_permissions('extras.view_customfield')
|
self.add_permissions('extras.view_customfield')
|
||||||
@ -508,6 +511,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'multiselect_field': None,
|
'multiselect_field': None,
|
||||||
'object_field': None,
|
'object_field': None,
|
||||||
'multiobject_field': None,
|
'multiobject_field': None,
|
||||||
|
'decimal_field': None,
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_get_single_object_with_custom_field_data(self):
|
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']],
|
[obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
|
||||||
site2_cfvs['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):
|
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']],
|
[obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
|
||||||
cf_defaults['multiobject_field']
|
cf_defaults['multiobject_field']
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data['id'])
|
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['multiselect_field'], cf_defaults['multiselect_field'])
|
||||||
self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_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['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):
|
def test_create_single_object_with_values(self):
|
||||||
"""
|
"""
|
||||||
@ -603,6 +610,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'multiselect_field': ['Bar', 'Baz'],
|
'multiselect_field': ['Bar', 'Baz'],
|
||||||
'object_field': VLAN.objects.get(vid=2).pk,
|
'object_field': VLAN.objects.get(vid=2).pk,
|
||||||
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
|
'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')
|
url = reverse('dcim-api:site-list')
|
||||||
@ -628,6 +636,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
[obj['id'] for obj in response_cf['multiobject_field']],
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
data_cf['multiobject_field']
|
data_cf['multiobject_field']
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field'])
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data['id'])
|
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['multiselect_field'], data_cf['multiselect_field'])
|
||||||
self.assertEqual(site.custom_field_data['object_field'], data_cf['object_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['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):
|
def test_create_multiple_objects_with_defaults(self):
|
||||||
"""
|
"""
|
||||||
@ -690,6 +700,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
[obj['id'] for obj in response_cf['multiobject_field']],
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
cf_defaults['multiobject_field']
|
cf_defaults['multiobject_field']
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data[i]['id'])
|
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['multiselect_field'], cf_defaults['multiselect_field'])
|
||||||
self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_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['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):
|
def test_create_multiple_objects_with_values(self):
|
||||||
"""
|
"""
|
||||||
@ -721,6 +733,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'multiselect_field': ['Bar', 'Baz'],
|
'multiselect_field': ['Bar', 'Baz'],
|
||||||
'object_field': VLAN.objects.get(vid=2).pk,
|
'object_field': VLAN.objects.get(vid=2).pk,
|
||||||
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
|
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
|
||||||
|
'decimal_field': 456.78,
|
||||||
}
|
}
|
||||||
data = (
|
data = (
|
||||||
{
|
{
|
||||||
@ -764,6 +777,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
[obj['id'] for obj in response_cf['multiobject_field']],
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
custom_field_data['multiobject_field']
|
custom_field_data['multiobject_field']
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field'])
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site = Site.objects.get(pk=response.data[i]['id'])
|
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['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['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['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):
|
def test_update_single_object_with_values(self):
|
||||||
"""
|
"""
|
||||||
@ -814,6 +829,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
[obj['id'] for obj in response_cf['multiobject_field']],
|
[obj['id'] for obj in response_cf['multiobject_field']],
|
||||||
original_cfvs['multiobject_field']
|
original_cfvs['multiobject_field']
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response_cf['decimal_field'], data['custom_fields']['decimal_field'])
|
||||||
|
|
||||||
# Validate database data
|
# Validate database data
|
||||||
site2.refresh_from_db()
|
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['multiselect_field'], original_cfvs['multiselect_field'])
|
||||||
self.assertEqual(site2.custom_field_data['object_field'], original_cfvs['object_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['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):
|
def test_minimum_maximum_values_validation(self):
|
||||||
site2 = Site.objects.get(name='Site 2')
|
site2 = Site.objects.get(name='Site 2')
|
||||||
@ -896,6 +913,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=[
|
CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=[
|
||||||
'Choice A', 'Choice B', 'Choice C',
|
'Choice A', 'Choice B', 'Choice C',
|
||||||
]),
|
]),
|
||||||
|
CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL),
|
||||||
)
|
)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
cf.save()
|
cf.save()
|
||||||
@ -906,10 +924,10 @@ class CustomFieldImportTest(TestCase):
|
|||||||
Import a Site in CSV format, including a value for each CustomField.
|
Import a Site in CSV format, including a value for each CustomField.
|
||||||
"""
|
"""
|
||||||
data = (
|
data = (
|
||||||
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
|
('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"'),
|
('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"'),
|
('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', '', '', '', '', '', '', '', '', ''),
|
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', ''),
|
||||||
)
|
)
|
||||||
csv_data = '\n'.join(','.join(row) for row in data)
|
csv_data = '\n'.join(','.join(row) for row in data)
|
||||||
|
|
||||||
@ -919,7 +937,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
|
|
||||||
# Validate data for site 1
|
# Validate data for site 1
|
||||||
site1 = Site.objects.get(name='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['text'], 'ABC')
|
||||||
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
||||||
self.assertEqual(site1.custom_field_data['integer'], 123)
|
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['json'], {"foo": 123})
|
||||||
self.assertEqual(site1.custom_field_data['select'], 'Choice A')
|
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['multiselect'], ['Choice A', 'Choice B'])
|
||||||
|
self.assertEqual(site1.custom_field_data['decimal'], '123.45')
|
||||||
|
|
||||||
# Validate data for site 2
|
# Validate data for site 2
|
||||||
site2 = Site.objects.get(name='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['text'], 'DEF')
|
||||||
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
||||||
self.assertEqual(site2.custom_field_data['integer'], 456)
|
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['json'], {"bar": 456})
|
||||||
self.assertEqual(site2.custom_field_data['select'], 'Choice B')
|
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['multiselect'], ['Choice B', 'Choice C'])
|
||||||
|
self.assertEqual(site2.custom_field_data['decimal'], '456.78')
|
||||||
|
|
||||||
# No custom field data should be set for site 3
|
# No custom field data should be set for site 3
|
||||||
site3 = Site.objects.get(name='Site 3')
|
site3 = Site.objects.get(name='Site 3')
|
||||||
|
Loading…
Reference in New Issue
Block a user