django-bulk-hooks 0.1.267__tar.gz → 0.1.269__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bulk-hooks might be problematic. Click here for more details.
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/queryset.py +235 -112
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/LICENSE +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/README.md +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.267 → django_bulk_hooks-0.1.269}/django_bulk_hooks/registry.py +0 -0
|
@@ -485,19 +485,12 @@ class HookQuerySetMixin:
|
|
|
485
485
|
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
486
486
|
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
487
487
|
"""
|
|
488
|
-
model_cls = self.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
f"DEBUG: update_conflicts={update_conflicts}, unique_fields={unique_fields}, update_fields={update_fields}"
|
|
495
|
-
)
|
|
496
|
-
logger.debug(
|
|
497
|
-
f"bulk_create called for {model_cls.__name__} with {len(objs)} objects"
|
|
498
|
-
)
|
|
499
|
-
logger.debug(
|
|
500
|
-
f"update_conflicts={update_conflicts}, unique_fields={unique_fields}, update_fields={update_fields}"
|
|
488
|
+
model_cls, ctx, originals = self._setup_bulk_operation(
|
|
489
|
+
objs, "bulk_create", require_pks=False,
|
|
490
|
+
bypass_hooks=bypass_hooks, bypass_validation=bypass_validation,
|
|
491
|
+
update_conflicts=update_conflicts,
|
|
492
|
+
unique_fields=unique_fields,
|
|
493
|
+
update_fields=update_fields
|
|
501
494
|
)
|
|
502
495
|
|
|
503
496
|
# When you bulk insert you don't get the primary keys back (if it's an
|
|
@@ -518,26 +511,13 @@ class HookQuerySetMixin:
|
|
|
518
511
|
if not objs:
|
|
519
512
|
return objs
|
|
520
513
|
|
|
521
|
-
|
|
522
|
-
raise TypeError(
|
|
523
|
-
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
524
|
-
)
|
|
514
|
+
self._validate_objects(objs, require_pks=False, operation_name="bulk_create")
|
|
525
515
|
|
|
526
516
|
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
527
|
-
|
|
528
|
-
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
529
|
-
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
530
|
-
# identify that case as involving multiple tables.
|
|
531
|
-
is_mti = False
|
|
532
|
-
for parent in model_cls._meta.all_parents:
|
|
533
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
534
|
-
is_mti = True
|
|
535
|
-
break
|
|
517
|
+
is_mti = self._is_multi_table_inheritance()
|
|
536
518
|
|
|
537
519
|
# Fire hooks before DB ops
|
|
538
520
|
if not bypass_hooks:
|
|
539
|
-
ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
|
|
540
|
-
|
|
541
521
|
if update_conflicts and unique_fields:
|
|
542
522
|
# For upsert operations, we need to determine which records will be created vs updated
|
|
543
523
|
# Check which records already exist in the database based on unique fields
|
|
@@ -555,6 +535,9 @@ class HookQuerySetMixin:
|
|
|
555
535
|
for field_name in unique_fields:
|
|
556
536
|
if hasattr(obj, field_name):
|
|
557
537
|
unique_value[field_name] = getattr(obj, field_name)
|
|
538
|
+
elif hasattr(obj, field_name + '_id'):
|
|
539
|
+
# Handle ForeignKey fields where _id suffix is used
|
|
540
|
+
unique_value[field_name] = getattr(obj, field_name + '_id')
|
|
558
541
|
if unique_value:
|
|
559
542
|
unique_values.append(unique_value)
|
|
560
543
|
|
|
@@ -583,6 +566,9 @@ class HookQuerySetMixin:
|
|
|
583
566
|
for field_name in unique_fields:
|
|
584
567
|
if hasattr(obj, field_name):
|
|
585
568
|
obj_unique_value[field_name] = getattr(obj, field_name)
|
|
569
|
+
elif hasattr(obj, field_name + '_id'):
|
|
570
|
+
# Handle ForeignKey fields where _id suffix is used
|
|
571
|
+
obj_unique_value[field_name] = getattr(obj, field_name + '_id')
|
|
586
572
|
|
|
587
573
|
# Check if this record already exists using our bulk lookup
|
|
588
574
|
if obj_unique_value:
|
|
@@ -604,13 +590,7 @@ class HookQuerySetMixin:
|
|
|
604
590
|
|
|
605
591
|
# Handle auto_now fields intelligently for upsert operations
|
|
606
592
|
# Only set auto_now fields on records that will actually be created
|
|
607
|
-
|
|
608
|
-
for field in model_cls._meta.local_fields:
|
|
609
|
-
if hasattr(field, "auto_now") and field.auto_now:
|
|
610
|
-
field.pre_save(obj, add=True)
|
|
611
|
-
elif hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
612
|
-
if getattr(obj, field.name) is None:
|
|
613
|
-
field.pre_save(obj, add=True)
|
|
593
|
+
self._handle_auto_now_fields(new_records, add=True)
|
|
614
594
|
|
|
615
595
|
# For existing records, preserve their original auto_now values
|
|
616
596
|
# We'll need to fetch them from the database to preserve the timestamps
|
|
@@ -737,19 +717,12 @@ class HookQuerySetMixin:
|
|
|
737
717
|
else:
|
|
738
718
|
# For regular create operations, run create hooks before DB ops
|
|
739
719
|
# Handle auto_now fields normally for new records
|
|
740
|
-
|
|
741
|
-
for field in model_cls._meta.local_fields:
|
|
742
|
-
if hasattr(field, "auto_now") and field.auto_now:
|
|
743
|
-
field.pre_save(obj, add=True)
|
|
744
|
-
elif hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
745
|
-
if getattr(obj, field.name) is None:
|
|
746
|
-
field.pre_save(obj, add=True)
|
|
720
|
+
self._handle_auto_now_fields(objs, add=True)
|
|
747
721
|
|
|
748
722
|
if not bypass_validation:
|
|
749
723
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
750
724
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
751
725
|
else:
|
|
752
|
-
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
753
726
|
logger.debug("bulk_create bypassed hooks")
|
|
754
727
|
|
|
755
728
|
# For MTI models, we need to handle them specially
|
|
@@ -978,11 +951,11 @@ class HookQuerySetMixin:
|
|
|
978
951
|
if not objs:
|
|
979
952
|
return []
|
|
980
953
|
|
|
981
|
-
self._validate_objects(objs)
|
|
954
|
+
self._validate_objects(objs, require_pks=True, operation_name="bulk_update")
|
|
982
955
|
|
|
983
956
|
changed_fields = self._detect_changed_fields(objs)
|
|
984
957
|
is_mti = self._is_multi_table_inheritance()
|
|
985
|
-
hook_context, originals = self._init_hook_context(bypass_hooks, objs)
|
|
958
|
+
hook_context, originals = self._init_hook_context(bypass_hooks, objs, "bulk_update")
|
|
986
959
|
|
|
987
960
|
fields_set, auto_now_fields, custom_update_fields = self._prepare_update_fields(
|
|
988
961
|
changed_fields
|
|
@@ -1128,10 +1101,14 @@ class HookQuerySetMixin:
|
|
|
1128
1101
|
logger.debug("Built value_map for %d objects", len(value_map))
|
|
1129
1102
|
return value_map
|
|
1130
1103
|
|
|
1131
|
-
def _validate_objects(self, objs):
|
|
1104
|
+
def _validate_objects(self, objs, require_pks=False, operation_name="bulk_update"):
|
|
1132
1105
|
"""
|
|
1133
|
-
Validate that all objects are instances of this queryset's model
|
|
1134
|
-
|
|
1106
|
+
Validate that all objects are instances of this queryset's model.
|
|
1107
|
+
|
|
1108
|
+
Args:
|
|
1109
|
+
objs (list): Objects to validate
|
|
1110
|
+
require_pks (bool): Whether to validate that objects have primary keys
|
|
1111
|
+
operation_name (str): Name of the operation for error messages
|
|
1135
1112
|
"""
|
|
1136
1113
|
model_cls = self.model
|
|
1137
1114
|
|
|
@@ -1141,27 +1118,34 @@ class HookQuerySetMixin:
|
|
|
1141
1118
|
}
|
|
1142
1119
|
if invalid_types:
|
|
1143
1120
|
raise TypeError(
|
|
1144
|
-
f"
|
|
1121
|
+
f"{operation_name} expected instances of {model_cls.__name__}, "
|
|
1145
1122
|
f"but got {invalid_types}"
|
|
1146
1123
|
)
|
|
1147
1124
|
|
|
1148
|
-
# Primary key check
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1125
|
+
# Primary key check (optional, for operations that require saved objects)
|
|
1126
|
+
if require_pks:
|
|
1127
|
+
missing_pks = [obj for obj in objs if obj.pk is None]
|
|
1128
|
+
if missing_pks:
|
|
1129
|
+
raise ValueError(
|
|
1130
|
+
f"{operation_name} cannot operate on unsaved {model_cls.__name__} instances. "
|
|
1131
|
+
f"{len(missing_pks)} object(s) have no primary key."
|
|
1132
|
+
)
|
|
1155
1133
|
|
|
1156
1134
|
logger.debug(
|
|
1157
|
-
"Validated %d %s objects for
|
|
1135
|
+
"Validated %d %s objects for %s",
|
|
1158
1136
|
len(objs),
|
|
1159
1137
|
model_cls.__name__,
|
|
1138
|
+
operation_name,
|
|
1160
1139
|
)
|
|
1161
1140
|
|
|
1162
|
-
def _init_hook_context(self, bypass_hooks: bool, objs):
|
|
1141
|
+
def _init_hook_context(self, bypass_hooks: bool, objs, operation_name="bulk_update"):
|
|
1163
1142
|
"""
|
|
1164
|
-
Initialize the hook context for
|
|
1143
|
+
Initialize the hook context for bulk operations.
|
|
1144
|
+
|
|
1145
|
+
Args:
|
|
1146
|
+
bypass_hooks (bool): Whether to bypass hooks
|
|
1147
|
+
objs (list): List of objects being operated on
|
|
1148
|
+
operation_name (str): Name of the operation for logging
|
|
1165
1149
|
|
|
1166
1150
|
Returns:
|
|
1167
1151
|
(HookContext, list): The hook context and a placeholder list
|
|
@@ -1171,10 +1155,10 @@ class HookQuerySetMixin:
|
|
|
1171
1155
|
model_cls = self.model
|
|
1172
1156
|
|
|
1173
1157
|
if bypass_hooks:
|
|
1174
|
-
logger.debug("
|
|
1158
|
+
logger.debug("%s: hooks bypassed for %s", operation_name, model_cls.__name__)
|
|
1175
1159
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
1176
1160
|
else:
|
|
1177
|
-
logger.debug("
|
|
1161
|
+
logger.debug("%s: hooks enabled for %s", operation_name, model_cls.__name__)
|
|
1178
1162
|
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
1179
1163
|
|
|
1180
1164
|
# Keep `originals` aligned with objs to support later hook execution.
|
|
@@ -1234,13 +1218,14 @@ class HookQuerySetMixin:
|
|
|
1234
1218
|
|
|
1235
1219
|
return fields_set, auto_now_fields, custom_update_fields
|
|
1236
1220
|
|
|
1237
|
-
def _apply_auto_now_fields(self, objs, auto_now_fields):
|
|
1221
|
+
def _apply_auto_now_fields(self, objs, auto_now_fields, add=False):
|
|
1238
1222
|
"""
|
|
1239
1223
|
Apply the current timestamp to all auto_now fields on each object.
|
|
1240
1224
|
|
|
1241
1225
|
Args:
|
|
1242
|
-
objs (list[Model]): The model instances being
|
|
1226
|
+
objs (list[Model]): The model instances being processed.
|
|
1243
1227
|
auto_now_fields (list[str]): Field names that require auto_now behavior.
|
|
1228
|
+
add (bool): Whether this is for creation (add=True) or update (add=False).
|
|
1244
1229
|
"""
|
|
1245
1230
|
if not auto_now_fields:
|
|
1246
1231
|
return
|
|
@@ -1250,16 +1235,186 @@ class HookQuerySetMixin:
|
|
|
1250
1235
|
current_time = timezone.now()
|
|
1251
1236
|
|
|
1252
1237
|
logger.debug(
|
|
1253
|
-
"Setting auto_now fields %s to %s for %d objects",
|
|
1238
|
+
"Setting auto_now fields %s to %s for %d objects (add=%s)",
|
|
1254
1239
|
auto_now_fields,
|
|
1255
1240
|
current_time,
|
|
1256
1241
|
len(objs),
|
|
1242
|
+
add,
|
|
1257
1243
|
)
|
|
1258
1244
|
|
|
1259
1245
|
for obj in objs:
|
|
1260
1246
|
for field_name in auto_now_fields:
|
|
1261
1247
|
setattr(obj, field_name, current_time)
|
|
1262
1248
|
|
|
1249
|
+
def _handle_auto_now_fields(self, objs, add=False):
|
|
1250
|
+
"""
|
|
1251
|
+
Handle auto_now and auto_now_add fields for objects.
|
|
1252
|
+
|
|
1253
|
+
Args:
|
|
1254
|
+
objs (list[Model]): The model instances being processed.
|
|
1255
|
+
add (bool): Whether this is for creation (add=True) or update (add=False).
|
|
1256
|
+
|
|
1257
|
+
Returns:
|
|
1258
|
+
list[str]: Names of auto_now fields that were handled.
|
|
1259
|
+
"""
|
|
1260
|
+
model_cls = self.model
|
|
1261
|
+
handled_fields = []
|
|
1262
|
+
|
|
1263
|
+
for obj in objs:
|
|
1264
|
+
for field in model_cls._meta.local_fields:
|
|
1265
|
+
# Handle auto_now_add only during creation
|
|
1266
|
+
if add and hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
1267
|
+
if getattr(obj, field.name) is None:
|
|
1268
|
+
field.pre_save(obj, add=True)
|
|
1269
|
+
handled_fields.append(field.name)
|
|
1270
|
+
# Handle auto_now during creation or update
|
|
1271
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
1272
|
+
field.pre_save(obj, add=add)
|
|
1273
|
+
handled_fields.append(field.name)
|
|
1274
|
+
|
|
1275
|
+
return list(set(handled_fields)) # Remove duplicates
|
|
1276
|
+
|
|
1277
|
+
def _execute_hooks_with_operation(self, operation_func, validate_hook, before_hook, after_hook, objs, originals=None, ctx=None, bypass_hooks=False, bypass_validation=False):
|
|
1278
|
+
"""
|
|
1279
|
+
Execute the complete hook lifecycle around a database operation.
|
|
1280
|
+
|
|
1281
|
+
Args:
|
|
1282
|
+
operation_func (callable): The database operation to execute
|
|
1283
|
+
validate_hook: Hook constant for validation
|
|
1284
|
+
before_hook: Hook constant for before operation
|
|
1285
|
+
after_hook: Hook constant for after operation
|
|
1286
|
+
objs (list): Objects being operated on
|
|
1287
|
+
originals (list, optional): Original objects for comparison hooks
|
|
1288
|
+
ctx: Hook context
|
|
1289
|
+
bypass_hooks (bool): Whether to skip hooks
|
|
1290
|
+
bypass_validation (bool): Whether to skip validation hooks
|
|
1291
|
+
|
|
1292
|
+
Returns:
|
|
1293
|
+
The result of the database operation
|
|
1294
|
+
"""
|
|
1295
|
+
model_cls = self.model
|
|
1296
|
+
|
|
1297
|
+
# Run validation hooks first (if not bypassed)
|
|
1298
|
+
if not bypass_validation and validate_hook:
|
|
1299
|
+
engine.run(model_cls, validate_hook, objs, ctx=ctx)
|
|
1300
|
+
|
|
1301
|
+
# Run before hooks (if not bypassed)
|
|
1302
|
+
if not bypass_hooks and before_hook:
|
|
1303
|
+
engine.run(model_cls, before_hook, objs, originals, ctx=ctx)
|
|
1304
|
+
|
|
1305
|
+
# Execute the database operation
|
|
1306
|
+
result = operation_func()
|
|
1307
|
+
|
|
1308
|
+
# Run after hooks (if not bypassed)
|
|
1309
|
+
if not bypass_hooks and after_hook:
|
|
1310
|
+
engine.run(model_cls, after_hook, objs, originals, ctx=ctx)
|
|
1311
|
+
|
|
1312
|
+
return result
|
|
1313
|
+
|
|
1314
|
+
def _log_bulk_operation_start(self, operation_name, objs, **kwargs):
|
|
1315
|
+
"""
|
|
1316
|
+
Log the start of a bulk operation with consistent formatting.
|
|
1317
|
+
|
|
1318
|
+
Args:
|
|
1319
|
+
operation_name (str): Name of the operation (e.g., "bulk_create")
|
|
1320
|
+
objs (list): Objects being operated on
|
|
1321
|
+
**kwargs: Additional parameters to log
|
|
1322
|
+
"""
|
|
1323
|
+
model_cls = self.model
|
|
1324
|
+
|
|
1325
|
+
# Build parameter string for additional kwargs
|
|
1326
|
+
param_str = ""
|
|
1327
|
+
if kwargs:
|
|
1328
|
+
param_parts = []
|
|
1329
|
+
for key, value in kwargs.items():
|
|
1330
|
+
if isinstance(value, (list, tuple)):
|
|
1331
|
+
param_parts.append(f"{key}={value}")
|
|
1332
|
+
else:
|
|
1333
|
+
param_parts.append(f"{key}={value}")
|
|
1334
|
+
param_str = f", {', '.join(param_parts)}"
|
|
1335
|
+
|
|
1336
|
+
# Use both print and logger for consistency with existing patterns
|
|
1337
|
+
print(f"DEBUG: {operation_name} called for {model_cls.__name__} with {len(objs)} objects{param_str}")
|
|
1338
|
+
logger.debug(f"{operation_name} called for {model_cls.__name__} with {len(objs)} objects{param_str}")
|
|
1339
|
+
|
|
1340
|
+
def _execute_delete_hooks_with_operation(self, operation_func, objs, ctx=None, bypass_hooks=False, bypass_validation=False):
|
|
1341
|
+
"""
|
|
1342
|
+
Execute hooks for delete operations with special field caching logic.
|
|
1343
|
+
|
|
1344
|
+
Args:
|
|
1345
|
+
operation_func (callable): The delete operation to execute
|
|
1346
|
+
objs (list): Objects being deleted
|
|
1347
|
+
ctx: Hook context
|
|
1348
|
+
bypass_hooks (bool): Whether to skip hooks
|
|
1349
|
+
bypass_validation (bool): Whether to skip validation hooks
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
The result of the delete operation
|
|
1353
|
+
"""
|
|
1354
|
+
model_cls = self.model
|
|
1355
|
+
|
|
1356
|
+
# Run validation hooks first (if not bypassed)
|
|
1357
|
+
if not bypass_validation:
|
|
1358
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
1359
|
+
|
|
1360
|
+
# Run before hooks (if not bypassed)
|
|
1361
|
+
if not bypass_hooks:
|
|
1362
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
1363
|
+
|
|
1364
|
+
# Before deletion, ensure all related fields are properly cached
|
|
1365
|
+
# to avoid DoesNotExist errors in AFTER_DELETE hooks
|
|
1366
|
+
for obj in objs:
|
|
1367
|
+
if obj.pk is not None:
|
|
1368
|
+
# Cache all foreign key relationships by accessing them
|
|
1369
|
+
for field in model_cls._meta.fields:
|
|
1370
|
+
if (
|
|
1371
|
+
field.is_relation
|
|
1372
|
+
and not field.many_to_many
|
|
1373
|
+
and not field.one_to_many
|
|
1374
|
+
):
|
|
1375
|
+
try:
|
|
1376
|
+
# Access the related field to cache it before deletion
|
|
1377
|
+
getattr(obj, field.name)
|
|
1378
|
+
except Exception:
|
|
1379
|
+
# If we can't access the field (e.g., already deleted, no permission, etc.)
|
|
1380
|
+
# continue with other fields
|
|
1381
|
+
pass
|
|
1382
|
+
|
|
1383
|
+
# Execute the database operation
|
|
1384
|
+
result = operation_func()
|
|
1385
|
+
|
|
1386
|
+
# Run after hooks (if not bypassed)
|
|
1387
|
+
if not bypass_hooks:
|
|
1388
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
1389
|
+
|
|
1390
|
+
return result
|
|
1391
|
+
|
|
1392
|
+
def _setup_bulk_operation(self, objs, operation_name, require_pks=False, bypass_hooks=False, bypass_validation=False, **log_kwargs):
|
|
1393
|
+
"""
|
|
1394
|
+
Common setup logic for bulk operations.
|
|
1395
|
+
|
|
1396
|
+
Args:
|
|
1397
|
+
objs (list): Objects to operate on
|
|
1398
|
+
operation_name (str): Name of the operation for logging and validation
|
|
1399
|
+
require_pks (bool): Whether objects must have primary keys
|
|
1400
|
+
bypass_hooks (bool): Whether to bypass hooks
|
|
1401
|
+
bypass_validation (bool): Whether to bypass validation
|
|
1402
|
+
**log_kwargs: Additional parameters to log
|
|
1403
|
+
|
|
1404
|
+
Returns:
|
|
1405
|
+
tuple: (model_cls, ctx, originals)
|
|
1406
|
+
"""
|
|
1407
|
+
# Log operation start
|
|
1408
|
+
self._log_bulk_operation_start(operation_name, objs, **log_kwargs)
|
|
1409
|
+
|
|
1410
|
+
# Validate objects
|
|
1411
|
+
self._validate_objects(objs, require_pks=require_pks, operation_name=operation_name)
|
|
1412
|
+
|
|
1413
|
+
# Initialize hook context
|
|
1414
|
+
ctx, originals = self._init_hook_context(bypass_hooks, objs, operation_name)
|
|
1415
|
+
|
|
1416
|
+
return self.model, ctx, originals
|
|
1417
|
+
|
|
1263
1418
|
def _is_multi_table_inheritance(self) -> bool:
|
|
1264
1419
|
"""
|
|
1265
1420
|
Determine whether this model uses multi-table inheritance (MTI).
|
|
@@ -1271,7 +1426,7 @@ class HookQuerySetMixin:
|
|
|
1271
1426
|
logger.debug(
|
|
1272
1427
|
"%s detected as MTI model (parent: %s)",
|
|
1273
1428
|
model_cls.__name__,
|
|
1274
|
-
parent
|
|
1429
|
+
getattr(parent, "__name__", str(parent)),
|
|
1275
1430
|
)
|
|
1276
1431
|
return True
|
|
1277
1432
|
|
|
@@ -1793,56 +1948,24 @@ class HookQuerySetMixin:
|
|
|
1793
1948
|
if not objs:
|
|
1794
1949
|
return 0
|
|
1795
1950
|
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
)
|
|
1800
|
-
|
|
1801
|
-
logger.debug(
|
|
1802
|
-
f"bulk_delete {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
|
|
1951
|
+
model_cls, ctx, _ = self._setup_bulk_operation(
|
|
1952
|
+
objs, "bulk_delete", require_pks=True,
|
|
1953
|
+
bypass_hooks=bypass_hooks, bypass_validation=bypass_validation
|
|
1803
1954
|
)
|
|
1804
1955
|
|
|
1805
|
-
#
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
if
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
logger.debug("bulk_delete bypassed hooks")
|
|
1814
|
-
|
|
1815
|
-
# Before deletion, ensure all related fields are properly cached
|
|
1816
|
-
# to avoid DoesNotExist errors in AFTER_DELETE hooks
|
|
1817
|
-
if not bypass_hooks:
|
|
1818
|
-
for obj in objs:
|
|
1819
|
-
if obj.pk is not None:
|
|
1820
|
-
# Cache all foreign key relationships by accessing them
|
|
1821
|
-
for field in model_cls._meta.fields:
|
|
1822
|
-
if (
|
|
1823
|
-
field.is_relation
|
|
1824
|
-
and not field.many_to_many
|
|
1825
|
-
and not field.one_to_many
|
|
1826
|
-
):
|
|
1827
|
-
try:
|
|
1828
|
-
# Access the related field to cache it before deletion
|
|
1829
|
-
getattr(obj, field.name)
|
|
1830
|
-
except Exception:
|
|
1831
|
-
# If we can't access the field (e.g., already deleted, no permission, etc.)
|
|
1832
|
-
# continue with other fields
|
|
1833
|
-
pass
|
|
1834
|
-
|
|
1835
|
-
# Use Django's standard delete() method on the queryset
|
|
1836
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
1837
|
-
if pks:
|
|
1838
|
-
# Use the base manager to avoid recursion
|
|
1839
|
-
result = self.model._base_manager.filter(pk__in=pks).delete()[0]
|
|
1840
|
-
else:
|
|
1841
|
-
result = 0
|
|
1956
|
+
# Execute the database operation with hooks
|
|
1957
|
+
def delete_operation():
|
|
1958
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
1959
|
+
if pks:
|
|
1960
|
+
# Use the base manager to avoid recursion
|
|
1961
|
+
return self.model._base_manager.filter(pk__in=pks).delete()[0]
|
|
1962
|
+
else:
|
|
1963
|
+
return 0
|
|
1842
1964
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1965
|
+
result = self._execute_delete_hooks_with_operation(
|
|
1966
|
+
delete_operation, objs, ctx=ctx,
|
|
1967
|
+
bypass_hooks=bypass_hooks, bypass_validation=bypass_validation
|
|
1968
|
+
)
|
|
1846
1969
|
|
|
1847
1970
|
return result
|
|
1848
1971
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.269"
|
|
4
4
|
description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
|
|
5
5
|
authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
|
|
6
6
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|