1+ import contextvars
12import sys
23import 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
78from django .db .utils import DatabaseError , OperationalError , ProgrammingError
89from netbox .plugins import PluginConfig
910
1011from .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
1432class 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
185177config = CustomObjectsPluginConfig
0 commit comments