|
7 | 7 | from django.contrib.postgres.fields import ArrayField
|
8 | 8 | from django.core.validators import RegexValidator
|
9 | 9 | from django.db import models
|
10 |
| -from django.db.models.fields.related import ManyToManyDescriptor |
| 10 | +from django.db.models.fields.related import ForeignKey, ManyToManyDescriptor |
11 | 11 | from django.db.models.manager import Manager
|
12 | 12 | from django.utils.html import escape
|
13 | 13 | from django.utils.safestring import mark_safe
|
|
27 | 27 | from netbox_custom_objects.constants import APP_LABEL
|
28 | 28 |
|
29 | 29 |
|
| 30 | +class LazyForeignKey(ForeignKey): |
| 31 | + """ |
| 32 | + A ForeignKey field that can handle lazy model references. |
| 33 | + The target model is resolved after the model is fully generated. |
| 34 | + """ |
| 35 | + |
| 36 | + def __init__(self, to_model_name, *args, **kwargs): |
| 37 | + self._to_model_name = to_model_name |
| 38 | + super().__init__(to_model_name, *args, **kwargs) |
| 39 | + |
| 40 | + def contribute_to_class(self, cls, name, **kwargs): |
| 41 | + super().contribute_to_class(cls, name, **kwargs) |
| 42 | + # Mark this field for later resolution |
| 43 | + setattr(cls, f'_resolve_{name}_model', self._resolve_model) |
| 44 | + |
| 45 | + def _resolve_model(self, model): |
| 46 | + """Resolve the lazy reference to the actual model class.""" |
| 47 | + # Get the actual model class from the app registry |
| 48 | + from django.apps import apps |
| 49 | + actual_model = apps.get_model(self._to_model_name) |
| 50 | + # Update the field's references |
| 51 | + self.remote_field.model = actual_model |
| 52 | + self.to = actual_model |
| 53 | + |
| 54 | + |
30 | 55 | class FieldType:
|
31 | 56 |
|
32 | 57 | def get_display_value(self, instance, field_name):
|
@@ -332,22 +357,39 @@ def get_model_field(self, field, **kwargs):
|
332 | 357 | to_model = content_type.model
|
333 | 358 | kwargs.update({"default": field.default, "unique": field.unique})
|
334 | 359 |
|
335 |
| - # TODO: Handle pointing to object of same type (avoid infinite loop) |
| 360 | + # Handle self-referential fields by using string references |
336 | 361 | if content_type.app_label == APP_LABEL:
|
337 | 362 | from netbox_custom_objects.models import CustomObjectType
|
338 | 363 |
|
339 | 364 | custom_object_type_id = content_type.model.replace("table", "").replace(
|
340 | 365 | "model", ""
|
341 | 366 | )
|
342 | 367 | custom_object_type = CustomObjectType.objects.get(pk=custom_object_type_id)
|
343 |
| - model = custom_object_type.get_model() |
| 368 | + |
| 369 | + # Check if this is a self-referential field |
| 370 | + if custom_object_type.id == field.custom_object_type.id: |
| 371 | + # For self-referential fields, use LazyForeignKey to defer resolution |
| 372 | + model_name = f"{APP_LABEL}.{custom_object_type.get_table_model_name(custom_object_type.id)}" |
| 373 | + f = LazyForeignKey( |
| 374 | + model_name, |
| 375 | + null=True, |
| 376 | + blank=True, |
| 377 | + on_delete=models.CASCADE, |
| 378 | + **kwargs |
| 379 | + ) |
| 380 | + return f |
| 381 | + else: |
| 382 | + # For cross-referential fields, use skip_object_fields to avoid infinite loops |
| 383 | + model = custom_object_type.get_model(skip_object_fields=True) |
344 | 384 | else:
|
345 | 385 | # to_model = content_type.model_class()._meta.object_name
|
346 | 386 | to_ct = f"{content_type.app_label}.{to_model}"
|
347 | 387 | model = apps.get_model(to_ct)
|
| 388 | + |
348 | 389 | f = models.ForeignKey(
|
349 | 390 | model, null=True, blank=True, on_delete=models.CASCADE, **kwargs
|
350 | 391 | )
|
| 392 | + |
351 | 393 | return f
|
352 | 394 |
|
353 | 395 | def get_form_field(self, field, for_csv_import=False, **kwargs):
|
@@ -404,11 +446,23 @@ def render_table_column(self, value):
|
404 | 446 |
|
405 | 447 | def get_serializer_field(self, field, **kwargs):
|
406 | 448 | related_model_class = field.related_object_type.model_class()
|
407 |
| - if not related_model_class: |
408 |
| - raise NotImplementedError("Custom object serializers not implemented") |
409 |
| - serializer = get_serializer_for_model(related_model_class) |
| 449 | + if related_model_class._meta.app_label == APP_LABEL: |
| 450 | + from netbox_custom_objects.api.serializers import get_serializer_class |
| 451 | + serializer = get_serializer_class(related_model_class, skip_object_fields=True) |
| 452 | + else: |
| 453 | + serializer = get_serializer_for_model(related_model_class) |
410 | 454 | return serializer(required=field.required, nested=True)
|
411 | 455 |
|
| 456 | + def after_model_generation(self, instance, model, field_name): |
| 457 | + """ |
| 458 | + Resolve lazy references after the model is fully generated. |
| 459 | + This ensures that self-referential fields point to the correct model class. |
| 460 | + """ |
| 461 | + # Check if this field has a resolution method |
| 462 | + resolve_method = getattr(model, f'_resolve_{field_name}_model', None) |
| 463 | + if resolve_method: |
| 464 | + resolve_method(model) |
| 465 | + |
412 | 466 |
|
413 | 467 | class CustomManyToManyManager(Manager):
|
414 | 468 | def __init__(self, instance=None, field_name=None):
|
|
0 commit comments