Skip to content

Commit 2dd8d17

Browse files
authored
171 fix ObjectChange for update not showing on restart (#178)
1 parent 305ed3d commit 2dd8d17

File tree

4 files changed

+82
-26
lines changed

4 files changed

+82
-26
lines changed

netbox_custom_objects/__init__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from django.db.utils import DatabaseError, OperationalError, ProgrammingError
77
from netbox.plugins import PluginConfig
88

9+
from .constants import APP_LABEL as APP_LABEL
10+
911

1012
def is_running_migration():
1113
"""
@@ -106,11 +108,14 @@ def get_models(self, include_auto_created=False, include_swapped=False):
106108

107109
custom_object_types = CustomObjectType.objects.all()
108110
for custom_type in custom_object_types:
109-
# Only yield already cached models during discovery
110-
if CustomObjectType.is_model_cached(custom_type.id):
111-
model = CustomObjectType.get_cached_model(custom_type.id)
112-
if model:
113-
yield model
111+
model = custom_type.get_model()
112+
if model:
113+
yield model
114+
115+
# If include_auto_created is True, also yield through models
116+
if include_auto_created and hasattr(model, '_through_models'):
117+
for through_model in model._through_models:
118+
yield through_model
114119

115120

116121
config = CustomObjectsPluginConfig

netbox_custom_objects/field_types.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from utilities.templatetags.builtins.filters import linkify, render_markdown
3131

3232
from netbox_custom_objects.constants import APP_LABEL
33+
from netbox_custom_objects.utilities import generate_model
3334

3435

3536
class LazyForeignKey(ForeignKey):
@@ -405,11 +406,15 @@ def get_model_field(self, field, **kwargs):
405406
if custom_object_type.id == field.custom_object_type.id:
406407
# For self-referential fields, use LazyForeignKey to defer resolution
407408
model_name = f"{APP_LABEL}.{custom_object_type.get_table_model_name(custom_object_type.id)}"
409+
# Generate a unique related_name to prevent reverse accessor conflicts
410+
table_model_name = field.custom_object_type.get_table_model_name(field.custom_object_type.id).lower()
411+
related_name = f"{table_model_name}_{field.name}_set"
408412
f = LazyForeignKey(
409413
model_name,
410414
null=True,
411415
blank=True,
412416
on_delete=models.CASCADE,
417+
related_name=related_name,
413418
**field_kwargs
414419
)
415420
return f
@@ -420,11 +425,17 @@ def get_model_field(self, field, **kwargs):
420425
# We're in a circular reference, don't call get_model() to prevent recursion
421426
# Use a string reference instead
422427
model_name = f"{APP_LABEL}.{custom_object_type.get_table_model_name(custom_object_type.id)}"
428+
# Generate a unique related_name to prevent reverse accessor conflicts
429+
table_model_name = field.custom_object_type.get_table_model_name(
430+
field.custom_object_type.id
431+
).lower()
432+
related_name = f"{table_model_name}_{field.name}_set"
423433
f = models.ForeignKey(
424434
model_name,
425435
null=True,
426436
blank=True,
427437
on_delete=models.CASCADE,
438+
related_name=related_name,
428439
**field_kwargs
429440
)
430441
return f
@@ -435,8 +446,11 @@ def get_model_field(self, field, **kwargs):
435446
to_ct = f"{content_type.app_label}.{to_model}"
436447
model = apps.get_model(to_ct)
437448

449+
# Generate a unique related_name to prevent reverse accessor conflicts
450+
table_model_name = field.custom_object_type.get_table_model_name(field.custom_object_type.id).lower()
451+
related_name = f"{table_model_name}_{field.name}_set"
438452
f = models.ForeignKey(
439-
model, null=True, blank=True, on_delete=models.CASCADE, **field_kwargs
453+
model, null=True, blank=True, on_delete=models.CASCADE, related_name=related_name, **field_kwargs
440454
)
441455

442456
return f
@@ -719,7 +733,7 @@ def get_through_model(self, field, model_string):
719733
),
720734
}
721735

722-
return type(field.through_model_name, (models.Model,), attrs)
736+
return generate_model(field.through_model_name, (models.Model,), attrs)
723737

724738
def get_model_field(self, field, **kwargs):
725739
"""

netbox_custom_objects/models.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import decimal
22
import re
3-
import warnings
43
from datetime import date, datetime
54

65
import django_filters
@@ -51,6 +50,7 @@
5150

5251
from netbox_custom_objects.constants import APP_LABEL, RESERVED_FIELD_NAMES
5352
from netbox_custom_objects.field_types import FIELD_TYPE_CLASS
53+
from netbox_custom_objects.utilities import generate_model
5454

5555

5656
class UniquenessConstraintTestError(Exception):
@@ -421,6 +421,9 @@ def _after_model_generation(self, attrs, model):
421421
# Get the set of fields that were skipped due to recursion
422422
skipped_fields = attrs.get("_skipped_fields", set())
423423

424+
# Collect through models during after_model_generation
425+
through_models = []
426+
424427
for field_object in all_field_objects.values():
425428
field_name = field_object["name"]
426429

@@ -432,15 +435,28 @@ def _after_model_generation(self, attrs, model):
432435
# Fields might be skipped due to recursion prevention
433436
if hasattr(model._meta, 'get_field'):
434437
try:
435-
model._meta.get_field(field_name)
438+
field = model._meta.get_field(field_name)
436439
# Field exists, process it
437440
field_object["type"].after_model_generation(
438441
field_object["field"], model, field_name
439442
)
443+
444+
# Collect through models from M2M fields
445+
if hasattr(field, 'remote_field') and hasattr(field.remote_field, 'through'):
446+
through_model = field.remote_field.through
447+
# Only collect custom through models, not auto-created Django ones
448+
if (through_model and through_model not in through_models and
449+
hasattr(through_model._meta, 'app_label') and
450+
through_model._meta.app_label == APP_LABEL):
451+
through_models.append(through_model)
452+
440453
except Exception:
441454
# Field doesn't exist (likely skipped due to recursion), skip processing
442455
continue
443456

457+
# Store through models on the model for yielding in get_models()
458+
model._through_models = through_models
459+
444460
def get_collision_safe_order_id_idx_name(self):
445461
return f"tbl_order_id_{self.id}_idx"
446462

@@ -601,22 +617,14 @@ def wrapped_post_through_setup(self, cls):
601617

602618
TM.post_through_setup = wrapped_post_through_setup
603619

604-
# Suppress RuntimeWarning about model already being registered
605-
# TODO: Remove this once we have a better way to handle model registration
606-
with warnings.catch_warnings():
607-
warnings.filterwarnings(
608-
"ignore", category=RuntimeWarning, message=".*was already registered.*"
620+
try:
621+
model = generate_model(
622+
str(model_name),
623+
(CustomObject, models.Model),
624+
attrs,
609625
)
610-
611-
try:
612-
model = type(
613-
str(model_name),
614-
(CustomObject, models.Model),
615-
attrs,
616-
)
617-
finally:
618-
# Restore the original method
619-
TM.post_through_setup = original_post_through_setup
626+
finally:
627+
TM.post_through_setup = original_post_through_setup
620628

621629
# Register the main model with Django's app registry
622630
try:
@@ -634,6 +642,9 @@ def wrapped_post_through_setup(self, cls):
634642
# Cache the generated model
635643
if not no_cache:
636644
self._model_cache[self.id] = model
645+
# Do the clear cache now that we have it in the cache so there
646+
# is no recursion.
647+
apps.clear_cache()
637648

638649
# Register the serializer for this model
639650
if not manytomany_models:
@@ -663,6 +674,7 @@ def create_model(self):
663674
# Ensure the ContentType exists and is immediately available
664675
ct = self.get_or_create_content_type()
665676
features = get_model_features(model)
677+
ct.features = features + ['branching']
666678
ct.public = True
667679
ct.features = features
668680
ct.save()
@@ -1423,7 +1435,7 @@ def save(self, *args, **kwargs):
14231435
"managed": True,
14241436
},
14251437
)
1426-
old_through_model = type(
1438+
old_through_model = generate_model(
14271439
f"TempOld{self.original.through_model_name}",
14281440
(models.Model,),
14291441
{
@@ -1454,7 +1466,7 @@ def save(self, *args, **kwargs):
14541466
"managed": True,
14551467
},
14561468
)
1457-
new_through_model = type(
1469+
new_through_model = generate_model(
14581470
f"TempNew{self.through_model_name}",
14591471
(models.Model,),
14601472
{

netbox_custom_objects/utilities.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import warnings
2+
13
from django.apps import apps
24

35
from netbox_custom_objects.constants import APP_LABEL
46

57
__all__ = (
68
"AppsProxy",
9+
"generate_model",
710
"get_viewname",
811
)
912

@@ -83,3 +86,25 @@ def get_viewname(model, action=None, rest_api=False):
8386
viewname = f"{viewname}_{action}"
8487

8588
return viewname
89+
90+
91+
def generate_model(*args, **kwargs):
92+
"""
93+
Create a model.
94+
"""
95+
# Monkey patch apps.clear_cache to do nothing
96+
apps.clear_cache = lambda: None
97+
98+
# Suppress RuntimeWarning about model already being registered
99+
# TODO: Remove this once we have a better way to handle model registration
100+
with warnings.catch_warnings():
101+
warnings.filterwarnings(
102+
"ignore", category=RuntimeWarning, message=".*was already registered.*"
103+
)
104+
105+
try:
106+
model = type(*args, **kwargs)
107+
finally:
108+
apps.clear_cache = apps.clear_cache
109+
110+
return model

0 commit comments

Comments
 (0)