diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index e7536a654..df9437634 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -275,6 +275,15 @@ Stores a numeric integer. Options include: * `min_value` - Minimum value * `max_value` - Maximum value +### DecimalVar + +Stores a numeric decimal. Options include: + +* `min_value` - Minimum value +* `max_value` - Maximum value +* `max_digits` - Maximum number of digits, including decimal places +* `decimal_places` - Number of decimal places + ### BooleanVar A true/false flag. This field has no options beyond the defaults listed above. diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index f96066fb1..420ef4882 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -31,6 +31,7 @@ __all__ = ( 'DateTimeVar', 'FileVar', 'IntegerVar', + 'DecimalVar', 'IPAddressVar', 'IPAddressWithMaskVar', 'IPNetworkVar', @@ -135,6 +136,26 @@ class IntegerVar(ScriptVariable): self.field_attrs['max_value'] = max_value +class DecimalVar(ScriptVariable): + """ + Decimal representation. Can enforce minimum/maximum values, maximum digits and decimal places. + """ + form_field = forms.DecimalField + + def __init__(self, min_value=None, max_value=None, max_digits=None, decimal_places=None, *args, **kwargs,): + super().__init__(*args, **kwargs) + + # Optional constraints + if min_value: + self.field_attrs["min_value"] = min_value + if max_value: + self.field_attrs["max_value"] = max_value + if max_digits: + self.field_attrs["max_digits"] = max_digits + if decimal_places: + self.field_attrs["decimal_places"] = decimal_places + + class BooleanVar(ScriptVariable): """ Boolean representation (true/false). Renders as a checkbox. diff --git a/netbox/extras/tests/test_scripts.py b/netbox/extras/tests/test_scripts.py index 17eb5a31a..4f5d0187a 100644 --- a/netbox/extras/tests/test_scripts.py +++ b/netbox/extras/tests/test_scripts.py @@ -1,6 +1,7 @@ import logging import tempfile from datetime import date, datetime, timezone +from decimal import Decimal from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase @@ -138,6 +139,54 @@ class ScriptVariablesTest(TestCase): self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data['var1'], data['var1']) + def test_decimalvar(self): + + class TestScript(Script): + + var1 = DecimalVar( + min_value=-100.500, + max_value=100.500, + max_digits=6, + decimal_places=3, + required=False + ) + + var2 = DecimalVar( + max_digits=3, + decimal_places=1, + required=False + ) + + # Validate min_value enforcement + data = {'var1': -100.501} + form = TestScript().as_form(data, None) + self.assertFalse(form.is_valid()) + self.assertIn('var1', form.errors) + + # Validate max_value enforcement + data = {'var1': 100.501} + form = TestScript().as_form(data, None) + self.assertFalse(form.is_valid()) + self.assertIn('var1', form.errors) + + # Validate max_digits enforcement + data = {'var2': 123.4} + form = TestScript().as_form(data, None) + self.assertFalse(form.is_valid()) + self.assertIn('var2', form.errors) + + # Validate decimal_places + data = {'var2': 1.23} + form = TestScript().as_form(data, None) + self.assertFalse(form.is_valid()) + self.assertIn('var2', form.errors) + + # Validate valid data + data = {'var1': '50.123'} + form = TestScript().as_form(data, None) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['var1'], Decimal(data['var1'])) + def test_booleanvar(self): class TestScript(Script):