Explorar el Código

Fixes #20221: JSON CustomField does not coerce `{}` to null

This fix actually fixes this for all valid JSON values that evaluate to
`False` in Python when loaded and cast to bool:
`bool(json.loads(<val>))`.

- `{}`
- `[]`
- `0`
- `False`

This does not change the behavior of `()` or `""` which are both
explicitly cited as "empty" values on `JSONField`.
Jason Novinger hace 5 meses
padre
commit
fcb380b5c5

+ 1 - 1
netbox/extras/models/customfields.py

@@ -538,7 +538,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
 
         # JSON
         elif self.type == CustomFieldTypeChoices.TYPE_JSON:
-            field = JSONField(required=required, initial=json.dumps(initial) if initial else None)
+            field = JSONField(required=required, initial=json.dumps(initial) if initial is not None else None)
 
         # Object
         elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

+ 56 - 0
netbox/extras/tests/test_customfields.py

@@ -1,7 +1,9 @@
 import datetime
+import json
 from decimal import Decimal
 
 from django.core.exceptions import ValidationError
+from django.test import tag
 from django.urls import reverse
 from rest_framework import status
 
@@ -269,6 +271,60 @@ class CustomFieldTest(TestCase):
         instance.refresh_from_db()
         self.assertIsNone(instance.custom_field_data.get(cf.name))
 
+    @tag('regression')
+    def test_json_field_falsy_defaults(self):
+        """Test that falsy JSON default values are properly handled"""
+        falsy_test_cases = [
+            ({}, 'empty_dict'),
+            ([], 'empty_array'),
+            (0, 'zero'),
+            (False, 'false_bool'),
+            ("", 'empty_string'),
+        ]
+
+        for default, suffix in falsy_test_cases:
+            with self.subTest(default=default, suffix=suffix):
+                cf = CustomField.objects.create(
+                    name=f'json_falsy_{suffix}',
+                    type=CustomFieldTypeChoices.TYPE_JSON,
+                    default=default,
+                    required=False
+                )
+                cf.object_types.set([self.object_type])
+
+                instance = Site.objects.create(name=f'Test Site {suffix}', slug=f'test-site-{suffix}')
+
+                self.assertIsNotNone(instance.custom_field_data)
+                self.assertIn(cf.name, instance.custom_field_data)
+
+                instance.refresh_from_db()
+                stored = instance.custom_field_data[cf.name]
+                self.assertEqual(stored, default)
+
+    @tag('regression')
+    def test_json_field_falsy_to_form_field(self):
+        """Test form field generation preserves falsy defaults"""
+        falsy_test_cases = (
+            ({}, json.dumps({}), 'empty_dict'),
+            ([], json.dumps([]), 'empty_array'),
+            (0, json.dumps(0), 'zero'),
+            (False, json.dumps(False), 'false_bool'),
+            ("", '""', 'empty_string'),
+        )
+
+        for default, expected, suffix in falsy_test_cases:
+            with self.subTest(default=default, expected=expected, suffix=suffix):
+                cf = CustomField.objects.create(
+                    name=f'json_falsy_{suffix}',
+                    type=CustomFieldTypeChoices.TYPE_JSON,
+                    default=default,
+                    required=False
+                )
+                cf.object_types.set([self.object_type])
+
+                form_field = cf.to_form_field(set_initial=True)
+                self.assertEqual(form_field.initial, expected)
+
     def test_select_field(self):
         CHOICES = (
             ('a', 'Option A'),

+ 1 - 1
netbox/utilities/templates/builtins/customfield_value.html

@@ -14,7 +14,7 @@
   {{ value|isodatetime }}
 {% elif customfield.type == 'url' and value %}
   <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
-{% elif customfield.type == 'json' and value %}
+{% elif customfield.type == 'json' and value is not None %}
   <pre>{{ value|json }}</pre>
 {% elif customfield.type == 'multiselect' and value %}
   {{ value|join:", " }}