Skip to content

Commit 392e830

Browse files
authored
Merge pull request #339 from netboxlabs/338-exception
#338 remove global exception handler on _all_migrations_applied
2 parents d186fa8 + 791b42b commit 392e830

File tree

1 file changed

+69
-77
lines changed

1 file changed

+69
-77
lines changed

netbox_custom_objects/__init__.py

Lines changed: 69 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1+
import contextvars
12
import sys
23
import warnings
34

4-
from django.core.management import call_command
5-
from django.core.management.base import CommandError
6-
from django.db import transaction
5+
from django.db import connection, transaction
6+
from django.db.migrations.recorder import MigrationRecorder
7+
from django.db.models.signals import pre_migrate, post_migrate
78
from django.db.utils import DatabaseError, OperationalError, ProgrammingError
89
from netbox.plugins import PluginConfig
910

1011
from .constants import APP_LABEL as APP_LABEL
1112

13+
# Context variable to track if we're currently running migrations
14+
_is_migrating = contextvars.ContextVar('is_migrating', default=False)
15+
16+
# Minimum migration required for the plugin to function properly
17+
# Update this when adding migrations that add fields to the plugin's models
18+
REQUIRED_MIGRATION = '0003_ensure_fk_constraints'
19+
20+
21+
def _migration_started(sender, **kwargs):
22+
"""Signal handler for pre_migrate - sets the migration flag."""
23+
_is_migrating.set(True)
24+
25+
26+
def _migration_finished(sender, **kwargs):
27+
"""Signal handler for post_migrate - clears the migration flag."""
28+
_is_migrating.set(False)
29+
1230

1331
# Plugin Configuration
1432
class CustomObjectsPluginConfig(PluginConfig):
@@ -29,44 +47,45 @@ class CustomObjectsPluginConfig(PluginConfig):
2947
template_extensions = "template_content.template_extensions"
3048

3149
@staticmethod
32-
def _is_running_migration():
33-
"""
34-
Check if the code is currently running during a Django migration.
50+
def _should_skip_dynamic_model_creation():
3551
"""
36-
# Check if 'makemigrations' or 'migrate' command is in sys.argv
37-
return any(cmd in sys.argv for cmd in ["makemigrations", "migrate"])
52+
Determine if dynamic model creation should be skipped.
3853
39-
@staticmethod
40-
def _is_running_test():
41-
"""
42-
Check if the code is currently running during Django tests.
43-
"""
44-
# Check if 'test' command is in sys.argv
45-
return "test" in sys.argv
54+
Returns True if dynamic models should not be created/loaded due to:
55+
- Currently running migrations
56+
- Running tests
57+
- Required migration not yet applied
4658
47-
@staticmethod
48-
def _all_migrations_applied():
49-
"""
50-
Check if all migrations for this app are applied.
51-
Returns True if all migrations are applied, False otherwise.
59+
Returns False if it's safe to proceed with dynamic model creation.
5260
"""
61+
# Skip if currently running migrations
62+
if _is_migrating.get():
63+
return True
64+
65+
# Skip if running tests
66+
if "test" in sys.argv:
67+
return True
68+
69+
# Skip if required migration hasn't been applied yet
5370
try:
54-
call_command(
55-
"migrate",
56-
APP_LABEL,
57-
check=True,
58-
dry_run=True,
59-
interactive=False,
60-
verbosity=0,
61-
)
71+
recorder = MigrationRecorder(connection)
72+
applied_migrations = recorder.applied_migrations()
73+
if ('netbox_custom_objects', REQUIRED_MIGRATION) not in applied_migrations:
74+
return True
75+
except (DatabaseError, OperationalError, ProgrammingError):
76+
# If we can't check, assume migrations haven't been run
6277
return True
63-
except (CommandError, Exception):
64-
return False
78+
79+
return False
6580

6681
def ready(self):
6782
from .models import CustomObjectType
6883
from netbox_custom_objects.api.serializers import get_serializer_class
6984

85+
# Connect migration signals to track migration state
86+
pre_migrate.connect(_migration_started)
87+
post_migrate.connect(_migration_finished)
88+
7089
# Suppress warnings about database calls during app initialization
7190
with warnings.catch_warnings():
7291
warnings.filterwarnings(
@@ -76,29 +95,16 @@ def ready(self):
7695
"ignore", category=UserWarning, message=".*database.*"
7796
)
7897

79-
# Skip database calls if running during migration or if table doesn't exist
80-
# or if not all migrations have been applied yet
81-
if (
82-
self._is_running_migration()
83-
or not self._all_migrations_applied()
84-
):
98+
# Skip database calls if dynamic models can't be created yet
99+
if self._should_skip_dynamic_model_creation():
85100
super().ready()
86101
return
87102

88-
try:
89-
with transaction.atomic():
90-
qs = CustomObjectType.objects.all()
91-
for obj in qs:
92-
model = obj.get_model()
93-
get_serializer_class(model)
94-
except (DatabaseError, OperationalError, ProgrammingError):
95-
# Only suppress exceptions during tests when schema may not match model
96-
# During normal operation, re-raise to alert of actual problems
97-
if self._is_running_test():
98-
# The transaction.atomic() block will automatically rollback
99-
pass
100-
else:
101-
raise
103+
with transaction.atomic():
104+
qs = CustomObjectType.objects.all()
105+
for obj in qs:
106+
model = obj.get_model()
107+
get_serializer_class(model)
102108

103109
super().ready()
104110

@@ -148,38 +154,24 @@ def get_models(self, include_auto_created=False, include_swapped=False):
148154
"ignore", category=UserWarning, message=".*database.*"
149155
)
150156

151-
# Skip custom object type model loading if running during migration
152-
# or if not all migrations have been applied yet
153-
if (
154-
self._is_running_migration()
155-
or not self._all_migrations_applied()
156-
):
157+
# Skip custom object type model loading if dynamic models can't be created yet
158+
if self._should_skip_dynamic_model_creation():
157159
return
158160

159161
# Add custom object type models
160162
from .models import CustomObjectType
161163

162-
try:
163-
with transaction.atomic():
164-
custom_object_types = CustomObjectType.objects.all()
165-
for custom_type in custom_object_types:
166-
model = custom_type.get_model()
167-
if model:
168-
yield model
169-
170-
# If include_auto_created is True, also yield through models
171-
if include_auto_created and hasattr(model, '_through_models'):
172-
for through_model in model._through_models:
173-
yield through_model
174-
except (DatabaseError, OperationalError, ProgrammingError):
175-
# Only suppress exceptions during tests when schema may not match model
176-
# (e.g., cache_timestamp column doesn't exist yet during test setup)
177-
# During normal operation, re-raise to alert of actual problems
178-
if self._is_running_test():
179-
# The transaction.atomic() block will automatically rollback
180-
pass
181-
else:
182-
raise
164+
with transaction.atomic():
165+
custom_object_types = CustomObjectType.objects.all()
166+
for custom_type in custom_object_types:
167+
model = custom_type.get_model()
168+
if model:
169+
yield model
170+
171+
# If include_auto_created is True, also yield through models
172+
if include_auto_created and hasattr(model, '_through_models'):
173+
for through_model in model._through_models:
174+
yield through_model
183175

184176

185177
config = CustomObjectsPluginConfig

0 commit comments

Comments
 (0)