ソースを参照

save custom field data

Arthur 15 時間 前
コミット
90d73b8266
2 ファイル変更37 行追加0 行削除
  1. 30 0
      netbox/circuits/tests/test_models.py
  2. 7 0
      netbox/netbox/models/features.py

+ 30 - 0
netbox/circuits/tests/test_models.py

@@ -8,6 +8,8 @@ from circuits.models import Circuit, CircuitTermination, CircuitType, Provider,
 from core.choices import ObjectChangeActionChoices
 from core.choices import ObjectChangeActionChoices
 from core.models import ObjectChange
 from core.models import ObjectChange
 from dcim.models import Location, Region, Site, SiteGroup
 from dcim.models import Location, Region, Site, SiteGroup
+from extras.choices import CustomFieldStatusChoices, CustomFieldTypeChoices
+from extras.models import CustomField
 from netbox.context_managers import event_tracking
 from netbox.context_managers import event_tracking
 from users.models import User
 from users.models import User
 
 
@@ -573,3 +575,31 @@ class CircuitTerminationChangeLoggingTestCase(TestCase):
         self._tracked(self.circuits[0].delete)
         self._tracked(self.circuits[0].delete)
 
 
         self.assertFalse(self._circuit_changes(self.circuits[0]).exists())
         self.assertFalse(self._circuit_changes(self.circuits[0]).exists())
+
+    @tag('regression')  # Ref: #23134
+    def test_pointer_update_persists_populated_custom_field_defaults(self):
+        # The pointer is written with update_fields, but CustomFieldsMixin.save() populates defaults
+        # into custom_field_data, which the change log serializes. Both must reach the database.
+        custom_field = CustomField.objects.create(
+            name='probe_field',
+            type=CustomFieldTypeChoices.TYPE_TEXT,
+            default='default-value',
+            status=CustomFieldStatusChoices.STATUS_ACTIVE,
+        )
+        custom_field.object_types.set([ContentType.objects.get_for_model(Circuit)])
+        CustomField.objects.clear_cache()
+        Circuit.objects.filter(pk=self.circuits[0].pk).update(custom_field_data={})
+        ObjectChange.objects.all().delete()
+
+        self._tracked(lambda: CircuitTermination.objects.create(
+            circuit=self.circuits[0], term_side='A', termination=self.sites[0],
+        ))
+
+        self.circuits[0].refresh_from_db()
+        self.assertEqual(self.circuits[0].custom_field_data, {'probe_field': 'default-value'})
+
+        changes = self._circuit_changes(self.circuits[0])
+        self.assertEqual(changes.count(), 1)
+        self.assertEqual(
+            changes[0].postchange_data['custom_fields'], self.circuits[0].custom_field_data
+        )

+ 7 - 0
netbox/netbox/models/features.py

@@ -347,9 +347,16 @@ class CustomFieldsMixin(models.Model):
         # covers fields still being provisioned as well as active ones, so that an object created
         # covers fields still being provisioned as well as active ones, so that an object created
         # while a new field is being backfilled does not miss its default (see
         # while a new field is being backfilled does not miss its default (see
         # CustomFieldManager.get_defaults_for_model()).
         # CustomFieldManager.get_defaults_for_model()).
+        populated = False
         for name, default in CustomField.objects.get_defaults_for_model(self).items():
         for name, default in CustomField.objects.get_defaults_for_model(self).items():
             if name not in self.custom_field_data:
             if name not in self.custom_field_data:
                 self.custom_field_data[name] = default
                 self.custom_field_data[name] = default
+                populated = True
+
+        # A partial save must still write any default populated above, or it would reach the change
+        # log (which serializes the instance) without ever reaching the database.
+        if populated and kwargs.get('update_fields') is not None:
+            kwargs['update_fields'] = [*kwargs['update_fields'], 'custom_field_data']
 
 
         super().save(*args, **kwargs)
         super().save(*args, **kwargs)