mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-24 08:25:17 -06:00
Store decimal custom field values natively
This commit is contained in:
parent
1e5b20970a
commit
7f38aa1c68
@ -498,7 +498,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
|
||||
# Validate decimal
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||
if type(value) is not decimal.Decimal:
|
||||
try:
|
||||
decimal.Decimal(value)
|
||||
except decimal.InvalidOperation:
|
||||
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}")
|
||||
|
@ -1,4 +1,5 @@
|
||||
import decimal
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
@ -401,7 +402,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo'),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC'),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='integer_field', default=123),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default='123.45'),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False),
|
||||
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'),
|
||||
@ -452,7 +453,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
custom_fields[0].name: 'bar',
|
||||
custom_fields[1].name: 'DEF',
|
||||
custom_fields[2].name: 456,
|
||||
custom_fields[3].name: '456.78',
|
||||
custom_fields[3].name: Decimal('456.78'),
|
||||
custom_fields[4].name: True,
|
||||
custom_fields[5].name: '2020-01-02',
|
||||
custom_fields[6].name: 'http://example.com/2',
|
||||
@ -603,7 +604,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
'text_field': 'bar',
|
||||
'longtext_field': 'blah blah blah',
|
||||
'integer_field': 456,
|
||||
'decimal_field': '456.78',
|
||||
'decimal_field': 456.78,
|
||||
'boolean_field': True,
|
||||
'date_field': '2020-01-02',
|
||||
'url_field': 'http://example.com/2',
|
||||
@ -726,7 +727,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
'text_field': 'bar',
|
||||
'longtext_field': 'abcdefghij',
|
||||
'integer_field': 456,
|
||||
'decimal_field': '456.78',
|
||||
'decimal_field': 456.78,
|
||||
'boolean_field': True,
|
||||
'date_field': '2020-01-02',
|
||||
'url_field': 'http://example.com/2',
|
||||
@ -942,7 +943,7 @@ class CustomFieldImportTest(TestCase):
|
||||
self.assertEqual(site1.custom_field_data['text'], 'ABC')
|
||||
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
||||
self.assertEqual(site1.custom_field_data['integer'], 123)
|
||||
self.assertEqual(site1.custom_field_data['decimal'], '123.45')
|
||||
self.assertEqual(site1.custom_field_data['decimal'], 123.45)
|
||||
self.assertEqual(site1.custom_field_data['boolean'], True)
|
||||
self.assertEqual(site1.custom_field_data['date'], '2020-01-01')
|
||||
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
|
||||
@ -956,7 +957,7 @@ class CustomFieldImportTest(TestCase):
|
||||
self.assertEqual(site2.custom_field_data['text'], 'DEF')
|
||||
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
||||
self.assertEqual(site2.custom_field_data['integer'], 456)
|
||||
self.assertEqual(site2.custom_field_data['decimal'], '456.78')
|
||||
self.assertEqual(site2.custom_field_data['decimal'], 456.78)
|
||||
self.assertEqual(site2.custom_field_data['boolean'], False)
|
||||
self.assertEqual(site2.custom_field_data['date'], '2020-01-02')
|
||||
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
|
||||
@ -1092,14 +1093,20 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Exact text filtering
|
||||
cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
|
||||
cf = CustomField(
|
||||
name='cf4',
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Loose text filtering
|
||||
cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
||||
cf = CustomField(
|
||||
name='cf5',
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
@ -1109,24 +1116,38 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Exact URL filtering
|
||||
cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
|
||||
cf = CustomField(
|
||||
name='cf7',
|
||||
type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Loose URL filtering
|
||||
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
||||
cf = CustomField(
|
||||
name='cf8',
|
||||
type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Selection filtering
|
||||
cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
|
||||
cf = CustomField(
|
||||
name='cf9',
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
choices=['Foo', 'Bar', 'Baz']
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Multiselect filtering
|
||||
cf = CustomField(name='cf10', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'B', 'C', 'X'])
|
||||
cf = CustomField(
|
||||
name='cf10',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
choices=['A', 'B', 'C', 'X']
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
@ -1151,7 +1172,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', custom_field_data={
|
||||
'cf1': 100,
|
||||
'cf2': decimal.Decimal(100.25),
|
||||
'cf2': 100.1,
|
||||
'cf3': True,
|
||||
'cf4': 'foo',
|
||||
'cf5': 'foo',
|
||||
@ -1165,7 +1186,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
}),
|
||||
Site(name='Site 2', slug='site-2', custom_field_data={
|
||||
'cf1': 200,
|
||||
'cf2': decimal.Decimal(200.25),
|
||||
'cf2': 200.2,
|
||||
'cf3': True,
|
||||
'cf4': 'foobar',
|
||||
'cf5': 'foobar',
|
||||
@ -1179,7 +1200,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
}),
|
||||
Site(name='Site 3', slug='site-3', custom_field_data={
|
||||
'cf1': 300,
|
||||
'cf2': decimal.Decimal("300.25"),
|
||||
'cf2': 300.3,
|
||||
'cf3': False,
|
||||
'cf4': 'bar',
|
||||
'cf5': 'bar',
|
||||
|
@ -4,7 +4,6 @@ from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db.models.signals import class_prepared
|
||||
from django.dispatch import receiver
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from taggit.managers import TaggableManager
|
||||
@ -12,6 +11,7 @@ from taggit.managers import TaggableManager
|
||||
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
||||
from extras.utils import is_taggable, register_features
|
||||
from netbox.signals import post_clean
|
||||
from utilities.json import CustomFieldJSONEncoder
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
__all__ = (
|
||||
@ -124,7 +124,7 @@ class CustomFieldsMixin(models.Model):
|
||||
Enables support for custom fields.
|
||||
"""
|
||||
custom_field_data = models.JSONField(
|
||||
encoder=DjangoJSONEncoder,
|
||||
encoder=CustomFieldJSONEncoder,
|
||||
blank=True,
|
||||
default=dict
|
||||
)
|
||||
|
17
netbox/utilities/json.py
Normal file
17
netbox/utilities/json.py
Normal file
@ -0,0 +1,17 @@
|
||||
import decimal
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
__all__ = (
|
||||
'CustomFieldJSONEncoder',
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldJSONEncoder(DjangoJSONEncoder):
|
||||
"""
|
||||
Override Django's built-in JSON encoder to save decimal values as JSON numbers.
|
||||
"""
|
||||
def default(self, o):
|
||||
if isinstance(o, decimal.Decimal):
|
||||
return float(o)
|
||||
return super().default(o)
|
Loading…
Reference in New Issue
Block a user