1+ import logging
12import sys
23from random import choice
3- from typing import Any , Dict , List , Optional , TextIO
4+ from typing import Any , Dict , List , Optional , TextIO , Tuple
45
56from django .apps import apps
67from django .core .management .base import BaseCommand
910from data_generator .generators .data_generator import model_data_generator
1011from data_generator .settings .conf import config
1112
13+ logger = logging .getLogger (__name__ )
14+
1215
1316class Command (BaseCommand ):
1417 """Management command to generate fake data for models within a Django
@@ -147,60 +150,165 @@ def _get_target_models(self) -> List[Any]:
147150
148151 """
149152 return [
150- model
151- for model in apps .get_models ()
152- if f"{ model ._meta .app_label } .{ model .__name__ } "
153- not in config .exclude_models + self .django_models
154- and model ._meta .app_label not in config .exclude_apps
153+ model for model in apps .get_models () if not self ._is_model_excluded (model )
155154 ]
156155
157156 def generate_data_for_model (self , model : Any ) -> None :
158157 """Generate and bulk-create data instances for a specific model.
159158
160159 Args:
161160 ----
162- model (Model) : The Django model class to generate data for.
161+ model: The Django model class to generate data for.
163162
164163 """
165- model_name = model .__name__
164+ model_name = f" { model ._meta . app_label } . { model . __name__ } "
166165 if model in self .processed_models or model_name in self .django_models :
167166 return
168167
168+ self ._ensure_related_models_generated (model )
169169 batch_size = max (100 , self .num_records // 10 )
170170
171171 self .stdout .write (f"\n Generating data for model: { model_name } " )
172+
172173 unique_values : Dict = {}
173- self ._display_progress (0 , self .num_records , model_name )
174+ instances = []
175+ failed = False
176+
174177 for i in range (0 , self .num_records , batch_size ):
175- instances = [
176- model (** self ._generate_model_data (model , unique_values ))
177- for _ in range (min (batch_size , self .num_records - i ))
178- ]
178+ batch_instances , batch_failed = self ._generate_batch_instances (
179+ model , unique_values , min (batch_size , self .num_records - i )
180+ )
181+ if batch_failed :
182+ failed = True
183+ break
184+ instances .extend (batch_instances )
185+
186+ self ._display_progress (
187+ i + len (batch_instances ), self .num_records , model_name
188+ )
189+
190+ if failed :
191+ # Log error after progress bar to avoid disruption
192+ logger .error (
193+ "Failed to generate data for model '%s': "
194+ "No instances found for related model(s), which are required for data generation. "
195+ "Skipping data generation for this model.\n "
196+ "Hint: Ensure at least one instance exist for related models or remove them "
197+ "from 'DATA_GENERATOR_EXCLUDE_MODELS' or 'DATA_GENERATOR_EXCLUDE_APPS' in settings." ,
198+ model_name ,
199+ )
200+ self .stdout .write (
201+ self .style .ERROR (
202+ f"Skipped data generation for { model_name } due to missing related data.\n "
203+ "Hint: Create at least one instance for related models or remove them from excluded settings."
204+ )
205+ )
206+ return
207+
208+ if instances :
179209 model ._default_manager .bulk_create (instances , ignore_conflicts = True )
180- self ._display_progress (i + len (instances ), self .num_records , model_name )
181210
211+ self ._display_progress (self .num_records , self .num_records , model_name )
182212 self .stdout .write ("\n Done!" )
183213
184- # Mark the model as processed
185214 self .processed_models .add (model )
186- # Clear the related instances cache after generating data
187215 self .related_instance_cache .clear ()
188216
189- def _generate_model_data (self , model : Any , unique_values : Dict ) -> Dict [str , Any ]:
217+ def _generate_batch_instances (
218+ self , model : Any , unique_values : Dict , batch_size : int
219+ ) -> Tuple [List [Any ], bool ]:
220+ """Generate a batch of model instances.
221+
222+ Args:
223+ ----
224+ model: The Django model class to generate data for.
225+ unique_values: Dictionary to track unique field values.
226+ batch_size: Number of instances to generate in this batch.
227+
228+ Returns:
229+ -------
230+ Tuple containing:
231+ - List of generated instances.
232+ - Boolean indicating if generation failed.
233+
234+ """
235+ instances = []
236+ for _ in range (batch_size ):
237+ instance_data = self ._generate_model_data (model , unique_values )
238+ if instance_data is None :
239+ return [], True
240+ instances .append (model (** instance_data ))
241+ return instances , False
242+
243+ def _ensure_related_models_generated (self , model : Any ) -> None :
244+ """Ensure all related models have data generated before processing the
245+ current model.
246+
247+ This method recursively checks the fields of the given model for relationships
248+ (e.g., ForeignKey, OneToOneField) and generates data for any related models that
249+ have not yet been processed. This ensures that dependent data is available before
250+ generating data for the current model, avoiding issues with missing related instances.
251+
252+ Args:
253+ ----
254+ model: The Django model class to check for related models.
255+
256+ Returns:
257+ -------
258+ None
259+
260+ """
261+ for field in model ._meta .fields :
262+ if field .is_relation :
263+ related_model = field .related_model
264+ # Always generate data for OneToOneField relations, even if excluded
265+ if field .one_to_one and related_model not in self .processed_models :
266+ self .generate_data_for_model (related_model )
267+ # For other relations, only generate if not excluded and not processed
268+ elif (
269+ not self ._is_model_excluded (related_model )
270+ and related_model not in self .processed_models
271+ ):
272+ self .generate_data_for_model (related_model )
273+
274+ print (self ._is_model_excluded (related_model ))
275+
276+ def _is_model_excluded (self , model : Any ) -> bool :
277+ """Check if a model or its app is excluded from data generation.
278+
279+ Args:
280+ ----
281+ model: The Django model class to check.
282+
283+ Returns:
284+ -------
285+ bool: True if the model or its app is excluded, False otherwise.
286+
287+ """
288+ model_name = f"{ model ._meta .app_label } .{ model .__name__ } "
289+ return (
290+ model_name in config .exclude_models + self .django_models
291+ or model ._meta .app_label in config .exclude_apps
292+ )
293+
294+ def _generate_model_data (
295+ self , model : Any , unique_values : Dict
296+ ) -> Optional [Dict [str , Any ]]:
190297 """Generate a dictionary of field data for a model instance, handling
191298 unique and related fields.
192299
193300 Args:
194301 ----
195- model (Model): The Django model for which data is generated.
302+ model: The Django model for which data is generated.
303+ unique_values: Dictionary to track unique field values.
196304
197305 Returns:
198306 -------
199- Dict[str, Any]: A dictionary of field values for model instantiation.
307+ Dict[str, Any]: A dictionary of field values for model instantiation, or None if data cannot be generated .
200308
201309 """
202310 data : Dict = {}
203- model_name = model .__name__
311+ model_name = f" { model ._meta . app_label } . { model . __name__ } "
204312
205313 for field in model ._meta .fields :
206314 field_name = field .name
@@ -218,14 +326,16 @@ def _generate_model_data(self, model: Any, unique_values: Dict) -> Dict[str, Any
218326 generator = model_data_generator .field_generators .get (type (field ).__name__ )
219327 if field .is_relation :
220328 related_model = field .related_model
221- self .generate_data_for_model (related_model )
222329 if related_model not in self .related_instance_cache :
223330 self .related_instance_cache [related_model ] = list (
224331 related_model ._default_manager .order_by ("-id" ).values_list (
225332 "id" , flat = True
226333 )[: self .num_records ]
227334 )
228335
336+ if not self .related_instance_cache [related_model ]:
337+ return None
338+
229339 rel_id_field = f"{ field .name } _id"
230340 if field .one_to_one :
231341 data [rel_id_field ] = self .get_unique_rel_instance (related_model )
@@ -274,11 +384,9 @@ def get_unique_rel_instance(self, model: Any) -> Optional[int]:
274384 Optional[int]: A unique instance ID or None if no instances exist.
275385
276386 """
277- if self .related_instance_cache [model ]:
278- instance_id = choice (self .related_instance_cache [model ])
279- self .related_instance_cache [model ].remove (instance_id )
280- return instance_id
281- return None
387+ instance_id = choice (self .related_instance_cache [model ])
388+ self .related_instance_cache [model ].remove (instance_id )
389+ return instance_id
282390
283391 def _confirm_models (self , related_models : List [Any ]) -> bool :
284392 """Display the list of models for the user to review and ask for
@@ -327,7 +435,7 @@ def _confirm_proceed(self) -> bool:
327435 """
328436 while True :
329437 user_input = (
330- input ("\n Type 'y' to proceed or 'n' to cancel the operation:" )
438+ input ("\n Type 'y' to proceed or 'n' to cancel the operation: " )
331439 .strip ()
332440 .lower ()
333441 )
0 commit comments