14
14
from django .db import connection , IntegrityError , models , transaction
15
15
from django .db .models import Q
16
16
from django .db .models .functions import Lower
17
- from django .db .models .signals import pre_delete
17
+ from django .db .models .signals import pre_delete , post_save
18
+ from django .dispatch import receiver
18
19
from django .urls import reverse
19
20
from django .utils .translation import gettext_lazy as _
20
21
from core .signals import handle_deleted_object
@@ -671,17 +672,8 @@ def save(self, *args, **kwargs):
671
672
needs_db_create = self ._state .adding
672
673
673
674
super ().save (* args , ** kwargs )
674
- if needs_db_create :
675
- # If creating a new object, get or create the ObjectType
676
- content_type_name = self .get_table_model_name (self .id ).lower ()
677
- ct , created = ObjectType .objects .get_or_create (
678
- app_label = APP_LABEL ,
679
- model = content_type_name
680
- )
681
- # Force a refresh to ensure it's available in the current transaction
682
- # ct.refresh_from_db()
683
- self .object_type = ct
684
675
676
+ if needs_db_create :
685
677
self .create_model ()
686
678
else :
687
679
# Clear the model cache when the CustomObjectType is modified
@@ -694,7 +686,7 @@ def delete(self, *args, **kwargs):
694
686
model = self .get_model ()
695
687
696
688
# Delete all CustomObjectTypeFields that reference this CustomObjectType
697
- for field in CustomObjectTypeField .objects .filter (related_object_type = self .content_type ):
689
+ for field in CustomObjectTypeField .objects .filter (related_object_type = self .object_type ):
698
690
field .delete ()
699
691
700
692
object_type = ObjectType .objects .get_for_model (model )
@@ -710,6 +702,19 @@ def delete(self, *args, **kwargs):
710
702
pre_delete .connect (handle_deleted_object )
711
703
712
704
705
+ @receiver (post_save , sender = CustomObjectType )
706
+ def custom_object_type_post_save_handler (sender , instance , created , ** kwargs ):
707
+ if created :
708
+ # If creating a new object, get or create the ObjectType
709
+ content_type_name = instance .get_table_model_name (instance .id ).lower ()
710
+ ct , created = ObjectType .objects .get_or_create (
711
+ app_label = APP_LABEL ,
712
+ model = content_type_name
713
+ )
714
+ instance .object_type = ct
715
+ instance .save ()
716
+
717
+
713
718
class CustomObjectTypeField (CloningMixin , ExportTemplatesMixin , ChangeLoggedModel ):
714
719
custom_object_type = models .ForeignKey (
715
720
CustomObjectType , on_delete = models .CASCADE , related_name = "fields"
@@ -1118,6 +1123,83 @@ def clean(self):
1118
1123
}
1119
1124
)
1120
1125
1126
+ # Check for recursion in object and multiobject fields
1127
+ if (self .type in (
1128
+ CustomFieldTypeChoices .TYPE_OBJECT ,
1129
+ CustomFieldTypeChoices .TYPE_MULTIOBJECT ,
1130
+ ) and self .related_object_type_id and
1131
+ self .related_object_type .app_label == APP_LABEL ):
1132
+ self ._check_recursion ()
1133
+
1134
+ def _check_recursion (self ):
1135
+ """
1136
+ Check for circular references in object and multiobject fields.
1137
+ Raises ValidationError if recursion is detected.
1138
+ """
1139
+ # Check if this field points to the same custom object type (self-referential)
1140
+ print (f"related_object_type_id: { self .related_object_type_id } , custom_object_type.object_type_id: { self .custom_object_type .object_type_id } " )
1141
+ breakpoint ()
1142
+ if self .related_object_type_id == self .custom_object_type .object_type_id :
1143
+ return # Self-referential fields are allowed
1144
+
1145
+ # Get the related custom object type directly from the object_type relationship
1146
+ try :
1147
+ related_custom_object_type = CustomObjectType .objects .get (object_type = self .related_object_type )
1148
+ except CustomObjectType .DoesNotExist :
1149
+ return # Not a custom object type, no recursion possible
1150
+
1151
+ # Check for circular references by traversing the dependency chain
1152
+ visited = {self .custom_object_type .id }
1153
+ if self ._has_circular_reference (related_custom_object_type , visited ):
1154
+ raise ValidationError (
1155
+ {
1156
+ "related_object_type" : _ (
1157
+ "Circular reference detected. This field would create a circular dependency "
1158
+ "between custom object types."
1159
+ )
1160
+ }
1161
+ )
1162
+
1163
+ def _has_circular_reference (self , custom_object_type , visited ):
1164
+ """
1165
+ Recursively check if there's a circular reference by following the dependency chain.
1166
+
1167
+ Args:
1168
+ custom_object_type: The CustomObjectType object to check
1169
+ visited: Set of custom object type IDs already visited in this traversal
1170
+
1171
+ Returns:
1172
+ bool: True if a circular reference is detected, False otherwise
1173
+ """
1174
+ # If we've already visited this type, we have a cycle
1175
+ if custom_object_type .id in visited :
1176
+ return True
1177
+
1178
+ # Add this type to visited set
1179
+ visited .add (custom_object_type .id )
1180
+
1181
+ # Check all object and multiobject fields in this custom object type
1182
+ for field in custom_object_type .fields .filter (
1183
+ type__in = [
1184
+ CustomFieldTypeChoices .TYPE_OBJECT ,
1185
+ CustomFieldTypeChoices .TYPE_MULTIOBJECT ,
1186
+ ],
1187
+ related_object_type__isnull = False ,
1188
+ related_object_type__app_label = APP_LABEL
1189
+ ):
1190
+
1191
+ # Get the related custom object type directly from the object_type relationship
1192
+ try :
1193
+ next_custom_object_type = CustomObjectType .objects .get (object_type = field .related_object_type )
1194
+ except CustomObjectType .DoesNotExist :
1195
+ continue
1196
+
1197
+ # Recursively check this dependency
1198
+ if self ._has_circular_reference (next_custom_object_type , visited ):
1199
+ return True
1200
+
1201
+ return False
1202
+
1121
1203
def serialize (self , value ):
1122
1204
"""
1123
1205
Prepare a value for storage as JSON data.
0 commit comments