django-bulk-hooks 0.1.266__tar.gz → 0.1.268__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.266 → django_bulk_hooks-0.1.268}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/manager.py +4 -6
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/queryset.py +480 -279
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/LICENSE +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/README.md +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.266 → django_bulk_hooks-0.1.268}/django_bulk_hooks/registry.py +0 -0
|
@@ -8,18 +8,18 @@ class BulkHookManager(models.Manager):
|
|
|
8
8
|
# Use super().get_queryset() to let Django and MRO build the queryset
|
|
9
9
|
# This ensures cooperation with other managers
|
|
10
10
|
base_queryset = super().get_queryset()
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
# If the base queryset already has hook functionality, return it as-is
|
|
13
13
|
if isinstance(base_queryset, HookQuerySetMixin):
|
|
14
14
|
return base_queryset
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
# Otherwise, create a new HookQuerySet with the same parameters
|
|
17
17
|
# This is much simpler and avoids dynamic class creation issues
|
|
18
18
|
return HookQuerySet(
|
|
19
19
|
model=base_queryset.model,
|
|
20
20
|
query=base_queryset.query,
|
|
21
21
|
using=base_queryset._db,
|
|
22
|
-
hints=base_queryset._hints
|
|
22
|
+
hints=base_queryset._hints,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
def bulk_create(
|
|
@@ -50,9 +50,7 @@ class BulkHookManager(models.Manager):
|
|
|
50
50
|
**kwargs,
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
-
def bulk_update(
|
|
54
|
-
self, objs, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
55
|
-
):
|
|
53
|
+
def bulk_update(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
56
54
|
"""
|
|
57
55
|
Delegate to QuerySet's bulk_update implementation.
|
|
58
56
|
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
@@ -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
|
|
@@ -604,13 +584,7 @@ class HookQuerySetMixin:
|
|
|
604
584
|
|
|
605
585
|
# Handle auto_now fields intelligently for upsert operations
|
|
606
586
|
# 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)
|
|
587
|
+
self._handle_auto_now_fields(new_records, add=True)
|
|
614
588
|
|
|
615
589
|
# For existing records, preserve their original auto_now values
|
|
616
590
|
# We'll need to fetch them from the database to preserve the timestamps
|
|
@@ -737,19 +711,12 @@ class HookQuerySetMixin:
|
|
|
737
711
|
else:
|
|
738
712
|
# For regular create operations, run create hooks before DB ops
|
|
739
713
|
# 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)
|
|
714
|
+
self._handle_auto_now_fields(objs, add=True)
|
|
747
715
|
|
|
748
716
|
if not bypass_validation:
|
|
749
717
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
750
718
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
751
719
|
else:
|
|
752
|
-
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
753
720
|
logger.debug("bulk_create bypassed hooks")
|
|
754
721
|
|
|
755
722
|
# For MTI models, we need to handle them specially
|
|
@@ -975,217 +942,491 @@ class HookQuerySetMixin:
|
|
|
975
942
|
|
|
976
943
|
@transaction.atomic
|
|
977
944
|
def bulk_update(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
945
|
+
if not objs:
|
|
946
|
+
return []
|
|
947
|
+
|
|
948
|
+
self._validate_objects(objs, require_pks=True, operation_name="bulk_update")
|
|
949
|
+
|
|
950
|
+
changed_fields = self._detect_changed_fields(objs)
|
|
951
|
+
is_mti = self._is_multi_table_inheritance()
|
|
952
|
+
hook_context, originals = self._init_hook_context(bypass_hooks, objs, "bulk_update")
|
|
953
|
+
|
|
954
|
+
fields_set, auto_now_fields, custom_update_fields = self._prepare_update_fields(
|
|
955
|
+
changed_fields
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
self._apply_auto_now_fields(objs, auto_now_fields)
|
|
959
|
+
self._apply_custom_update_fields(objs, custom_update_fields, fields_set)
|
|
960
|
+
|
|
961
|
+
if is_mti:
|
|
962
|
+
return self._mti_bulk_update(objs, list(fields_set), **kwargs)
|
|
963
|
+
else:
|
|
964
|
+
return self._single_table_bulk_update(
|
|
965
|
+
objs, fields_set, auto_now_fields, **kwargs
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
def _apply_custom_update_fields(self, objs, custom_update_fields, fields_set):
|
|
978
969
|
"""
|
|
979
|
-
|
|
980
|
-
|
|
970
|
+
Call pre_save() for custom fields that require update handling
|
|
971
|
+
(e.g., CurrentUserField) and update both the objects and the field set.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
objs (list[Model]): The model instances being updated.
|
|
975
|
+
custom_update_fields (list[Field]): Fields that define a pre_save() hook.
|
|
976
|
+
fields_set (set[str]): The overall set of fields to update, mutated in place.
|
|
981
977
|
"""
|
|
978
|
+
if not custom_update_fields:
|
|
979
|
+
return
|
|
980
|
+
|
|
982
981
|
model_cls = self.model
|
|
982
|
+
pk_field_names = [f.name for f in model_cls._meta.pk_fields]
|
|
983
983
|
|
|
984
|
-
|
|
985
|
-
|
|
984
|
+
logger.debug(
|
|
985
|
+
"Applying pre_save() on custom update fields: %s",
|
|
986
|
+
[f.name for f in custom_update_fields],
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
for obj in objs:
|
|
990
|
+
for field in custom_update_fields:
|
|
991
|
+
try:
|
|
992
|
+
# Call pre_save with add=False (since this is an update)
|
|
993
|
+
new_value = field.pre_save(obj, add=False)
|
|
994
|
+
|
|
995
|
+
# Only assign if pre_save returned something
|
|
996
|
+
if new_value is not None:
|
|
997
|
+
setattr(obj, field.name, new_value)
|
|
998
|
+
|
|
999
|
+
# Ensure this field is included in the update set
|
|
1000
|
+
if (
|
|
1001
|
+
field.name not in fields_set
|
|
1002
|
+
and field.name not in pk_field_names
|
|
1003
|
+
):
|
|
1004
|
+
fields_set.add(field.name)
|
|
986
1005
|
|
|
987
|
-
|
|
1006
|
+
logger.debug(
|
|
1007
|
+
"Custom field %s updated via pre_save() for object %s",
|
|
1008
|
+
field.name,
|
|
1009
|
+
obj.pk,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
except Exception as e:
|
|
1013
|
+
logger.warning(
|
|
1014
|
+
"Failed to call pre_save() on custom field %s for object %s: %s",
|
|
1015
|
+
field.name,
|
|
1016
|
+
getattr(obj, "pk", None),
|
|
1017
|
+
e,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
def _single_table_bulk_update(self, objs, fields_set, auto_now_fields, **kwargs):
|
|
1021
|
+
"""
|
|
1022
|
+
Perform bulk_update for single-table models, handling Django semantics
|
|
1023
|
+
for kwargs and setting a value map for hook execution.
|
|
1024
|
+
|
|
1025
|
+
Args:
|
|
1026
|
+
objs (list[Model]): The model instances being updated.
|
|
1027
|
+
fields_set (set[str]): The names of fields to update.
|
|
1028
|
+
auto_now_fields (list[str]): Names of auto_now fields included in update.
|
|
1029
|
+
**kwargs: Extra arguments (only Django-supported ones are passed through).
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
list[Model]: The updated model instances.
|
|
1033
|
+
"""
|
|
1034
|
+
# Strip out unsupported bulk_update kwargs
|
|
1035
|
+
django_kwargs = self._filter_django_kwargs(kwargs)
|
|
1036
|
+
|
|
1037
|
+
# Build a value map: {pk -> {field: raw_value}} for later hook use
|
|
1038
|
+
value_map = self._build_value_map(objs, fields_set, auto_now_fields)
|
|
1039
|
+
|
|
1040
|
+
if value_map:
|
|
1041
|
+
set_bulk_update_value_map(value_map)
|
|
1042
|
+
|
|
1043
|
+
try:
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
"Calling Django bulk_update for %d objects on fields %s",
|
|
1046
|
+
len(objs),
|
|
1047
|
+
list(fields_set),
|
|
1048
|
+
)
|
|
1049
|
+
return super().bulk_update(objs, list(fields_set), **django_kwargs)
|
|
1050
|
+
finally:
|
|
1051
|
+
# Always clear thread-local state
|
|
1052
|
+
set_bulk_update_value_map(None)
|
|
1053
|
+
|
|
1054
|
+
def _filter_django_kwargs(self, kwargs):
|
|
1055
|
+
"""
|
|
1056
|
+
Remove unsupported arguments before passing to Django's bulk_update.
|
|
1057
|
+
"""
|
|
1058
|
+
unsupported = {
|
|
1059
|
+
"unique_fields",
|
|
1060
|
+
"update_conflicts",
|
|
1061
|
+
"update_fields",
|
|
1062
|
+
"ignore_conflicts",
|
|
1063
|
+
}
|
|
1064
|
+
passthrough = {}
|
|
1065
|
+
for k, v in kwargs.items():
|
|
1066
|
+
if k in unsupported:
|
|
1067
|
+
logger.warning(
|
|
1068
|
+
"Parameter '%s' is not supported by bulk_update. "
|
|
1069
|
+
"It is only available for bulk_create UPSERT operations.",
|
|
1070
|
+
k,
|
|
1071
|
+
)
|
|
1072
|
+
elif k not in {"bypass_hooks", "bypass_validation"}:
|
|
1073
|
+
passthrough[k] = v
|
|
1074
|
+
return passthrough
|
|
1075
|
+
|
|
1076
|
+
def _build_value_map(self, objs, fields_set, auto_now_fields):
|
|
1077
|
+
"""
|
|
1078
|
+
Build a mapping of {pk -> {field_name: raw_value}} for hook processing.
|
|
1079
|
+
|
|
1080
|
+
Expressions are not included; only concrete values assigned on the object.
|
|
1081
|
+
"""
|
|
1082
|
+
value_map = {}
|
|
1083
|
+
for obj in objs:
|
|
1084
|
+
if obj.pk is None:
|
|
1085
|
+
continue # skip unsaved objects
|
|
1086
|
+
field_values = {}
|
|
1087
|
+
for field_name in fields_set:
|
|
1088
|
+
value = getattr(obj, field_name)
|
|
1089
|
+
field_values[field_name] = value
|
|
1090
|
+
if field_name in auto_now_fields:
|
|
1091
|
+
logger.debug("Object %s %s=%s", obj.pk, field_name, value)
|
|
1092
|
+
if field_values:
|
|
1093
|
+
value_map[obj.pk] = field_values
|
|
1094
|
+
|
|
1095
|
+
logger.debug("Built value_map for %d objects", len(value_map))
|
|
1096
|
+
return value_map
|
|
1097
|
+
|
|
1098
|
+
def _validate_objects(self, objs, require_pks=False, operation_name="bulk_update"):
|
|
1099
|
+
"""
|
|
1100
|
+
Validate that all objects are instances of this queryset's model.
|
|
1101
|
+
|
|
1102
|
+
Args:
|
|
1103
|
+
objs (list): Objects to validate
|
|
1104
|
+
require_pks (bool): Whether to validate that objects have primary keys
|
|
1105
|
+
operation_name (str): Name of the operation for error messages
|
|
1106
|
+
"""
|
|
1107
|
+
model_cls = self.model
|
|
1108
|
+
|
|
1109
|
+
# Type check
|
|
1110
|
+
invalid_types = {
|
|
1111
|
+
type(obj).__name__ for obj in objs if not isinstance(obj, model_cls)
|
|
1112
|
+
}
|
|
1113
|
+
if invalid_types:
|
|
988
1114
|
raise TypeError(
|
|
989
|
-
f"
|
|
1115
|
+
f"{operation_name} expected instances of {model_cls.__name__}, "
|
|
1116
|
+
f"but got {invalid_types}"
|
|
990
1117
|
)
|
|
991
1118
|
|
|
992
|
-
#
|
|
993
|
-
|
|
994
|
-
|
|
1119
|
+
# Primary key check (optional, for operations that require saved objects)
|
|
1120
|
+
if require_pks:
|
|
1121
|
+
missing_pks = [obj for obj in objs if obj.pk is None]
|
|
1122
|
+
if missing_pks:
|
|
1123
|
+
raise ValueError(
|
|
1124
|
+
f"{operation_name} cannot operate on unsaved {model_cls.__name__} instances. "
|
|
1125
|
+
f"{len(missing_pks)} object(s) have no primary key."
|
|
1126
|
+
)
|
|
995
1127
|
|
|
996
1128
|
logger.debug(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1129
|
+
"Validated %d %s objects for %s",
|
|
1130
|
+
len(objs),
|
|
1131
|
+
model_cls.__name__,
|
|
1132
|
+
operation_name,
|
|
1001
1133
|
)
|
|
1002
1134
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
for
|
|
1006
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
1007
|
-
is_mti = True
|
|
1008
|
-
break
|
|
1135
|
+
def _init_hook_context(self, bypass_hooks: bool, objs, operation_name="bulk_update"):
|
|
1136
|
+
"""
|
|
1137
|
+
Initialize the hook context for bulk operations.
|
|
1009
1138
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1139
|
+
Args:
|
|
1140
|
+
bypass_hooks (bool): Whether to bypass hooks
|
|
1141
|
+
objs (list): List of objects being operated on
|
|
1142
|
+
operation_name (str): Name of the operation for logging
|
|
1143
|
+
|
|
1144
|
+
Returns:
|
|
1145
|
+
(HookContext, list): The hook context and a placeholder list
|
|
1146
|
+
for 'originals', which can be populated later if needed for
|
|
1147
|
+
after_update hooks.
|
|
1148
|
+
"""
|
|
1149
|
+
model_cls = self.model
|
|
1150
|
+
|
|
1151
|
+
if bypass_hooks:
|
|
1152
|
+
logger.debug("%s: hooks bypassed for %s", operation_name, model_cls.__name__)
|
|
1016
1153
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1154
|
+
else:
|
|
1155
|
+
logger.debug("%s: hooks enabled for %s", operation_name, model_cls.__name__)
|
|
1156
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
1157
|
+
|
|
1158
|
+
# Keep `originals` aligned with objs to support later hook execution.
|
|
1159
|
+
originals = [None] * len(objs)
|
|
1160
|
+
|
|
1161
|
+
return ctx, originals
|
|
1020
1162
|
|
|
1021
|
-
|
|
1163
|
+
def _prepare_update_fields(self, changed_fields):
|
|
1164
|
+
"""
|
|
1165
|
+
Determine the final set of fields to update, including auto_now
|
|
1166
|
+
fields and custom fields that require pre_save() on updates.
|
|
1167
|
+
|
|
1168
|
+
Args:
|
|
1169
|
+
changed_fields (Iterable[str]): Fields detected as changed.
|
|
1170
|
+
|
|
1171
|
+
Returns:
|
|
1172
|
+
tuple:
|
|
1173
|
+
fields_set (set): All fields that should be updated.
|
|
1174
|
+
auto_now_fields (list[str]): Fields that require auto_now behavior.
|
|
1175
|
+
custom_update_fields (list[Field]): Fields with pre_save hooks to call.
|
|
1176
|
+
"""
|
|
1177
|
+
model_cls = self.model
|
|
1022
1178
|
fields_set = set(changed_fields)
|
|
1023
|
-
|
|
1024
|
-
|
|
1179
|
+
pk_field_names = [f.name for f in model_cls._meta.pk_fields]
|
|
1180
|
+
|
|
1025
1181
|
auto_now_fields = []
|
|
1026
|
-
custom_update_fields = []
|
|
1027
|
-
|
|
1028
|
-
f"Checking for auto_now and custom update fields in {model_cls.__name__}"
|
|
1029
|
-
)
|
|
1182
|
+
custom_update_fields = []
|
|
1183
|
+
|
|
1030
1184
|
for field in model_cls._meta.local_concrete_fields:
|
|
1031
|
-
#
|
|
1032
|
-
|
|
1033
|
-
if hasattr(field, "auto_now") and field.auto_now:
|
|
1034
|
-
logger.debug(f"Found auto_now field: {field.name}")
|
|
1035
|
-
print(f"DEBUG: Found auto_now field: {field.name}")
|
|
1185
|
+
# Handle auto_now fields
|
|
1186
|
+
if getattr(field, "auto_now", False):
|
|
1036
1187
|
if field.name not in fields_set and field.name not in pk_field_names:
|
|
1037
1188
|
fields_set.add(field.name)
|
|
1038
|
-
if field.name != field.attname:
|
|
1189
|
+
if field.name != field.attname: # handle attname vs name
|
|
1039
1190
|
fields_set.add(field.attname)
|
|
1040
1191
|
auto_now_fields.append(field.name)
|
|
1041
|
-
logger.debug(
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
f"DEBUG: Auto_now field {field.name} already in fields list or is PK"
|
|
1049
|
-
)
|
|
1050
|
-
elif hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
1051
|
-
logger.debug(f"Found auto_now_add field: {field.name} (skipping)")
|
|
1052
|
-
# Check for custom fields that might need pre_save() on update (like CurrentUserField)
|
|
1192
|
+
logger.debug("Added auto_now field %s to update set", field.name)
|
|
1193
|
+
|
|
1194
|
+
# Skip auto_now_add (only applies at creation time)
|
|
1195
|
+
elif getattr(field, "auto_now_add", False):
|
|
1196
|
+
continue
|
|
1197
|
+
|
|
1198
|
+
# Handle custom pre_save fields
|
|
1053
1199
|
elif hasattr(field, "pre_save"):
|
|
1054
|
-
# Only call pre_save on fields that aren't already being updated
|
|
1055
1200
|
if field.name not in fields_set and field.name not in pk_field_names:
|
|
1056
1201
|
custom_update_fields.append(field)
|
|
1057
|
-
logger.debug(
|
|
1058
|
-
|
|
1202
|
+
logger.debug(
|
|
1203
|
+
"Marked custom field %s for pre_save update", field.name
|
|
1204
|
+
)
|
|
1059
1205
|
|
|
1060
|
-
logger.debug(
|
|
1061
|
-
|
|
1206
|
+
logger.debug(
|
|
1207
|
+
"Prepared update fields: fields_set=%s, auto_now_fields=%s, custom_update_fields=%s",
|
|
1208
|
+
fields_set,
|
|
1209
|
+
auto_now_fields,
|
|
1210
|
+
[f.name for f in custom_update_fields],
|
|
1211
|
+
)
|
|
1062
1212
|
|
|
1063
|
-
|
|
1064
|
-
if auto_now_fields:
|
|
1065
|
-
from django.utils import timezone
|
|
1213
|
+
return fields_set, auto_now_fields, custom_update_fields
|
|
1066
1214
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
)
|
|
1071
|
-
logger.debug(
|
|
1072
|
-
f"Setting auto_now fields {auto_now_fields} to current time: {current_time}"
|
|
1073
|
-
)
|
|
1074
|
-
for obj in objs:
|
|
1075
|
-
for field_name in auto_now_fields:
|
|
1076
|
-
setattr(obj, field_name, current_time)
|
|
1077
|
-
print(
|
|
1078
|
-
f"DEBUG: Set {field_name} to {current_time} for object {obj.pk}"
|
|
1079
|
-
)
|
|
1215
|
+
def _apply_auto_now_fields(self, objs, auto_now_fields, add=False):
|
|
1216
|
+
"""
|
|
1217
|
+
Apply the current timestamp to all auto_now fields on each object.
|
|
1080
1218
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
)
|
|
1089
|
-
for obj in objs:
|
|
1090
|
-
for field in custom_update_fields:
|
|
1091
|
-
try:
|
|
1092
|
-
# Call pre_save with add=False to indicate this is an update
|
|
1093
|
-
new_value = field.pre_save(obj, add=False)
|
|
1094
|
-
# Only update the field if pre_save returned a new value
|
|
1095
|
-
if new_value is not None:
|
|
1096
|
-
setattr(obj, field.name, new_value)
|
|
1097
|
-
# Add this field to the update fields if it's not already there and not a primary key
|
|
1098
|
-
if (
|
|
1099
|
-
field.name not in fields_set
|
|
1100
|
-
and field.name not in pk_field_names
|
|
1101
|
-
):
|
|
1102
|
-
fields_set.add(field.name)
|
|
1103
|
-
logger.debug(
|
|
1104
|
-
f"Custom field {field.name} updated via pre_save() for object {obj.pk}"
|
|
1105
|
-
)
|
|
1106
|
-
print(
|
|
1107
|
-
f"DEBUG: Custom field {field.name} updated via pre_save() for object {obj.pk}"
|
|
1108
|
-
)
|
|
1109
|
-
except Exception as e:
|
|
1110
|
-
logger.warning(
|
|
1111
|
-
f"Failed to call pre_save() on custom field {field.name}: {e}"
|
|
1112
|
-
)
|
|
1113
|
-
print(
|
|
1114
|
-
f"DEBUG: Failed to call pre_save() on custom field {field.name}: {e}"
|
|
1115
|
-
)
|
|
1219
|
+
Args:
|
|
1220
|
+
objs (list[Model]): The model instances being processed.
|
|
1221
|
+
auto_now_fields (list[str]): Field names that require auto_now behavior.
|
|
1222
|
+
add (bool): Whether this is for creation (add=True) or update (add=False).
|
|
1223
|
+
"""
|
|
1224
|
+
if not auto_now_fields:
|
|
1225
|
+
return
|
|
1116
1226
|
|
|
1117
|
-
|
|
1118
|
-
if is_mti:
|
|
1119
|
-
result = self._mti_bulk_update(objs, list(fields_set), **kwargs)
|
|
1120
|
-
else:
|
|
1121
|
-
# For single-table models, use Django's built-in bulk_update
|
|
1122
|
-
# Filter out parameters that are not supported by Django's bulk_update
|
|
1123
|
-
unsupported_params = ["unique_fields", "update_conflicts", "update_fields", "ignore_conflicts"]
|
|
1124
|
-
django_kwargs = {}
|
|
1125
|
-
|
|
1126
|
-
# Check if all objects have primary keys set before proceeding
|
|
1127
|
-
if not all(obj._is_pk_set() for obj in objs):
|
|
1128
|
-
missing_pk_count = sum(1 for obj in objs if not obj._is_pk_set())
|
|
1129
|
-
logger.error(
|
|
1130
|
-
f"bulk_update failed: {missing_pk_count} out of {len(objs)} objects don't have primary keys set. "
|
|
1131
|
-
"All objects must be saved to the database before bulk_update can be used."
|
|
1132
|
-
)
|
|
1133
|
-
print(f"ERROR: {missing_pk_count} objects don't have primary keys set")
|
|
1134
|
-
raise ValueError(
|
|
1135
|
-
f"All bulk_update() objects must have a primary key set. "
|
|
1136
|
-
f"{missing_pk_count} out of {len(objs)} objects are missing primary keys."
|
|
1137
|
-
)
|
|
1227
|
+
from django.utils import timezone
|
|
1138
1228
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1229
|
+
current_time = timezone.now()
|
|
1230
|
+
|
|
1231
|
+
logger.debug(
|
|
1232
|
+
"Setting auto_now fields %s to %s for %d objects (add=%s)",
|
|
1233
|
+
auto_now_fields,
|
|
1234
|
+
current_time,
|
|
1235
|
+
len(objs),
|
|
1236
|
+
add,
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
for obj in objs:
|
|
1240
|
+
for field_name in auto_now_fields:
|
|
1241
|
+
setattr(obj, field_name, current_time)
|
|
1242
|
+
|
|
1243
|
+
def _handle_auto_now_fields(self, objs, add=False):
|
|
1244
|
+
"""
|
|
1245
|
+
Handle auto_now and auto_now_add fields for objects.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
objs (list[Model]): The model instances being processed.
|
|
1249
|
+
add (bool): Whether this is for creation (add=True) or update (add=False).
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
list[str]: Names of auto_now fields that were handled.
|
|
1253
|
+
"""
|
|
1254
|
+
model_cls = self.model
|
|
1255
|
+
handled_fields = []
|
|
1256
|
+
|
|
1257
|
+
for obj in objs:
|
|
1258
|
+
for field in model_cls._meta.local_fields:
|
|
1259
|
+
# Handle auto_now_add only during creation
|
|
1260
|
+
if add and hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
1261
|
+
if getattr(obj, field.name) is None:
|
|
1262
|
+
field.pre_save(obj, add=True)
|
|
1263
|
+
handled_fields.append(field.name)
|
|
1264
|
+
# Handle auto_now during creation or update
|
|
1265
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
1266
|
+
field.pre_save(obj, add=add)
|
|
1267
|
+
handled_fields.append(field.name)
|
|
1268
|
+
|
|
1269
|
+
return list(set(handled_fields)) # Remove duplicates
|
|
1270
|
+
|
|
1271
|
+
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):
|
|
1272
|
+
"""
|
|
1273
|
+
Execute the complete hook lifecycle around a database operation.
|
|
1274
|
+
|
|
1275
|
+
Args:
|
|
1276
|
+
operation_func (callable): The database operation to execute
|
|
1277
|
+
validate_hook: Hook constant for validation
|
|
1278
|
+
before_hook: Hook constant for before operation
|
|
1279
|
+
after_hook: Hook constant for after operation
|
|
1280
|
+
objs (list): Objects being operated on
|
|
1281
|
+
originals (list, optional): Original objects for comparison hooks
|
|
1282
|
+
ctx: Hook context
|
|
1283
|
+
bypass_hooks (bool): Whether to skip hooks
|
|
1284
|
+
bypass_validation (bool): Whether to skip validation hooks
|
|
1285
|
+
|
|
1286
|
+
Returns:
|
|
1287
|
+
The result of the database operation
|
|
1288
|
+
"""
|
|
1289
|
+
model_cls = self.model
|
|
1290
|
+
|
|
1291
|
+
# Run validation hooks first (if not bypassed)
|
|
1292
|
+
if not bypass_validation and validate_hook:
|
|
1293
|
+
engine.run(model_cls, validate_hook, objs, ctx=ctx)
|
|
1294
|
+
|
|
1295
|
+
# Run before hooks (if not bypassed)
|
|
1296
|
+
if not bypass_hooks and before_hook:
|
|
1297
|
+
engine.run(model_cls, before_hook, objs, originals, ctx=ctx)
|
|
1298
|
+
|
|
1299
|
+
# Execute the database operation
|
|
1300
|
+
result = operation_func()
|
|
1301
|
+
|
|
1302
|
+
# Run after hooks (if not bypassed)
|
|
1303
|
+
if not bypass_hooks and after_hook:
|
|
1304
|
+
engine.run(model_cls, after_hook, objs, originals, ctx=ctx)
|
|
1305
|
+
|
|
1306
|
+
return result
|
|
1307
|
+
|
|
1308
|
+
def _log_bulk_operation_start(self, operation_name, objs, **kwargs):
|
|
1309
|
+
"""
|
|
1310
|
+
Log the start of a bulk operation with consistent formatting.
|
|
1311
|
+
|
|
1312
|
+
Args:
|
|
1313
|
+
operation_name (str): Name of the operation (e.g., "bulk_create")
|
|
1314
|
+
objs (list): Objects being operated on
|
|
1315
|
+
**kwargs: Additional parameters to log
|
|
1316
|
+
"""
|
|
1317
|
+
model_cls = self.model
|
|
1318
|
+
|
|
1319
|
+
# Build parameter string for additional kwargs
|
|
1320
|
+
param_str = ""
|
|
1321
|
+
if kwargs:
|
|
1322
|
+
param_parts = []
|
|
1323
|
+
for key, value in kwargs.items():
|
|
1324
|
+
if isinstance(value, (list, tuple)):
|
|
1325
|
+
param_parts.append(f"{key}={value}")
|
|
1326
|
+
else:
|
|
1327
|
+
param_parts.append(f"{key}={value}")
|
|
1328
|
+
param_str = f", {', '.join(param_parts)}"
|
|
1329
|
+
|
|
1330
|
+
# Use both print and logger for consistency with existing patterns
|
|
1331
|
+
print(f"DEBUG: {operation_name} called for {model_cls.__name__} with {len(objs)} objects{param_str}")
|
|
1332
|
+
logger.debug(f"{operation_name} called for {model_cls.__name__} with {len(objs)} objects{param_str}")
|
|
1333
|
+
|
|
1334
|
+
def _execute_delete_hooks_with_operation(self, operation_func, objs, ctx=None, bypass_hooks=False, bypass_validation=False):
|
|
1335
|
+
"""
|
|
1336
|
+
Execute hooks for delete operations with special field caching logic.
|
|
1337
|
+
|
|
1338
|
+
Args:
|
|
1339
|
+
operation_func (callable): The delete operation to execute
|
|
1340
|
+
objs (list): Objects being deleted
|
|
1341
|
+
ctx: Hook context
|
|
1342
|
+
bypass_hooks (bool): Whether to skip hooks
|
|
1343
|
+
bypass_validation (bool): Whether to skip validation hooks
|
|
1344
|
+
|
|
1345
|
+
Returns:
|
|
1346
|
+
The result of the delete operation
|
|
1347
|
+
"""
|
|
1348
|
+
model_cls = self.model
|
|
1349
|
+
|
|
1350
|
+
# Run validation hooks first (if not bypassed)
|
|
1351
|
+
if not bypass_validation:
|
|
1352
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
1353
|
+
|
|
1354
|
+
# Run before hooks (if not bypassed)
|
|
1355
|
+
if not bypass_hooks:
|
|
1356
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
1357
|
+
|
|
1358
|
+
# Before deletion, ensure all related fields are properly cached
|
|
1359
|
+
# to avoid DoesNotExist errors in AFTER_DELETE hooks
|
|
1155
1360
|
for obj in objs:
|
|
1156
|
-
if obj.pk is None:
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
# Always clear after the internal update() path finishes
|
|
1177
|
-
set_bulk_update_value_map(None)
|
|
1178
|
-
logger.debug(f"Django bulk_update done: {result}")
|
|
1179
|
-
|
|
1180
|
-
# Note: We don't run AFTER_UPDATE hooks here to prevent double execution
|
|
1181
|
-
# The update() method will handle all hook execution based on thread-local state
|
|
1361
|
+
if obj.pk is not None:
|
|
1362
|
+
# Cache all foreign key relationships by accessing them
|
|
1363
|
+
for field in model_cls._meta.fields:
|
|
1364
|
+
if (
|
|
1365
|
+
field.is_relation
|
|
1366
|
+
and not field.many_to_many
|
|
1367
|
+
and not field.one_to_many
|
|
1368
|
+
):
|
|
1369
|
+
try:
|
|
1370
|
+
# Access the related field to cache it before deletion
|
|
1371
|
+
getattr(obj, field.name)
|
|
1372
|
+
except Exception:
|
|
1373
|
+
# If we can't access the field (e.g., already deleted, no permission, etc.)
|
|
1374
|
+
# continue with other fields
|
|
1375
|
+
pass
|
|
1376
|
+
|
|
1377
|
+
# Execute the database operation
|
|
1378
|
+
result = operation_func()
|
|
1379
|
+
|
|
1380
|
+
# Run after hooks (if not bypassed)
|
|
1182
1381
|
if not bypass_hooks:
|
|
1183
|
-
|
|
1184
|
-
else:
|
|
1185
|
-
logger.debug("bulk_update: hooks bypassed")
|
|
1382
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
1186
1383
|
|
|
1187
1384
|
return result
|
|
1188
1385
|
|
|
1386
|
+
def _setup_bulk_operation(self, objs, operation_name, require_pks=False, bypass_hooks=False, bypass_validation=False, **log_kwargs):
|
|
1387
|
+
"""
|
|
1388
|
+
Common setup logic for bulk operations.
|
|
1389
|
+
|
|
1390
|
+
Args:
|
|
1391
|
+
objs (list): Objects to operate on
|
|
1392
|
+
operation_name (str): Name of the operation for logging and validation
|
|
1393
|
+
require_pks (bool): Whether objects must have primary keys
|
|
1394
|
+
bypass_hooks (bool): Whether to bypass hooks
|
|
1395
|
+
bypass_validation (bool): Whether to bypass validation
|
|
1396
|
+
**log_kwargs: Additional parameters to log
|
|
1397
|
+
|
|
1398
|
+
Returns:
|
|
1399
|
+
tuple: (model_cls, ctx, originals)
|
|
1400
|
+
"""
|
|
1401
|
+
# Log operation start
|
|
1402
|
+
self._log_bulk_operation_start(operation_name, objs, **log_kwargs)
|
|
1403
|
+
|
|
1404
|
+
# Validate objects
|
|
1405
|
+
self._validate_objects(objs, require_pks=require_pks, operation_name=operation_name)
|
|
1406
|
+
|
|
1407
|
+
# Initialize hook context
|
|
1408
|
+
ctx, originals = self._init_hook_context(bypass_hooks, objs, operation_name)
|
|
1409
|
+
|
|
1410
|
+
return self.model, ctx, originals
|
|
1411
|
+
|
|
1412
|
+
def _is_multi_table_inheritance(self) -> bool:
|
|
1413
|
+
"""
|
|
1414
|
+
Determine whether this model uses multi-table inheritance (MTI).
|
|
1415
|
+
Returns True if the model has any concrete parent models other than itself.
|
|
1416
|
+
"""
|
|
1417
|
+
model_cls = self.model
|
|
1418
|
+
for parent in model_cls._meta.all_parents:
|
|
1419
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
1420
|
+
logger.debug(
|
|
1421
|
+
"%s detected as MTI model (parent: %s)",
|
|
1422
|
+
model_cls.__name__,
|
|
1423
|
+
getattr(parent, "__name__", str(parent)),
|
|
1424
|
+
)
|
|
1425
|
+
return True
|
|
1426
|
+
|
|
1427
|
+
logger.debug("%s is not an MTI model", model_cls.__name__)
|
|
1428
|
+
return False
|
|
1429
|
+
|
|
1189
1430
|
def _detect_modified_fields(self, new_instances, original_instances):
|
|
1190
1431
|
"""
|
|
1191
1432
|
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
@@ -1504,21 +1745,13 @@ class HookQuerySetMixin:
|
|
|
1504
1745
|
if inheritance_chain is None:
|
|
1505
1746
|
inheritance_chain = self._get_inheritance_chain()
|
|
1506
1747
|
|
|
1507
|
-
# Check if all objects have primary keys set before proceeding
|
|
1508
|
-
if not all(obj._is_pk_set() for obj in objs):
|
|
1509
|
-
missing_pk_count = sum(1 for obj in objs if not obj._is_pk_set())
|
|
1510
|
-
logger.error(
|
|
1511
|
-
f"MTI bulk_update failed: {missing_pk_count} out of {len(objs)} objects don't have primary keys set. "
|
|
1512
|
-
"All objects must be saved to the database before bulk_update can be used."
|
|
1513
|
-
)
|
|
1514
|
-
print(f"ERROR: {missing_pk_count} objects don't have primary keys set")
|
|
1515
|
-
raise ValueError(
|
|
1516
|
-
f"All bulk_update() objects must have a primary key set. "
|
|
1517
|
-
f"{missing_pk_count} out of {len(objs)} objects are missing primary keys."
|
|
1518
|
-
)
|
|
1519
|
-
|
|
1520
1748
|
# Remove custom hook kwargs and unsupported parameters before passing to Django internals
|
|
1521
|
-
unsupported_params = [
|
|
1749
|
+
unsupported_params = [
|
|
1750
|
+
"unique_fields",
|
|
1751
|
+
"update_conflicts",
|
|
1752
|
+
"update_fields",
|
|
1753
|
+
"ignore_conflicts",
|
|
1754
|
+
]
|
|
1522
1755
|
django_kwargs = {}
|
|
1523
1756
|
for k, v in kwargs.items():
|
|
1524
1757
|
if k in unsupported_params:
|
|
@@ -1709,56 +1942,24 @@ class HookQuerySetMixin:
|
|
|
1709
1942
|
if not objs:
|
|
1710
1943
|
return 0
|
|
1711
1944
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
)
|
|
1716
|
-
|
|
1717
|
-
logger.debug(
|
|
1718
|
-
f"bulk_delete {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
|
|
1945
|
+
model_cls, ctx, _ = self._setup_bulk_operation(
|
|
1946
|
+
objs, "bulk_delete", require_pks=True,
|
|
1947
|
+
bypass_hooks=bypass_hooks, bypass_validation=bypass_validation
|
|
1719
1948
|
)
|
|
1720
1949
|
|
|
1721
|
-
#
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
if
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
logger.debug("bulk_delete bypassed hooks")
|
|
1730
|
-
|
|
1731
|
-
# Before deletion, ensure all related fields are properly cached
|
|
1732
|
-
# to avoid DoesNotExist errors in AFTER_DELETE hooks
|
|
1733
|
-
if not bypass_hooks:
|
|
1734
|
-
for obj in objs:
|
|
1735
|
-
if obj.pk is not None:
|
|
1736
|
-
# Cache all foreign key relationships by accessing them
|
|
1737
|
-
for field in model_cls._meta.fields:
|
|
1738
|
-
if (
|
|
1739
|
-
field.is_relation
|
|
1740
|
-
and not field.many_to_many
|
|
1741
|
-
and not field.one_to_many
|
|
1742
|
-
):
|
|
1743
|
-
try:
|
|
1744
|
-
# Access the related field to cache it before deletion
|
|
1745
|
-
getattr(obj, field.name)
|
|
1746
|
-
except Exception:
|
|
1747
|
-
# If we can't access the field (e.g., already deleted, no permission, etc.)
|
|
1748
|
-
# continue with other fields
|
|
1749
|
-
pass
|
|
1750
|
-
|
|
1751
|
-
# Use Django's standard delete() method on the queryset
|
|
1752
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
1753
|
-
if pks:
|
|
1754
|
-
# Use the base manager to avoid recursion
|
|
1755
|
-
result = self.model._base_manager.filter(pk__in=pks).delete()[0]
|
|
1756
|
-
else:
|
|
1757
|
-
result = 0
|
|
1950
|
+
# Execute the database operation with hooks
|
|
1951
|
+
def delete_operation():
|
|
1952
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
1953
|
+
if pks:
|
|
1954
|
+
# Use the base manager to avoid recursion
|
|
1955
|
+
return self.model._base_manager.filter(pk__in=pks).delete()[0]
|
|
1956
|
+
else:
|
|
1957
|
+
return 0
|
|
1758
1958
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1959
|
+
result = self._execute_delete_hooks_with_operation(
|
|
1960
|
+
delete_operation, objs, ctx=ctx,
|
|
1961
|
+
bypass_hooks=bypass_hooks, bypass_validation=bypass_validation
|
|
1962
|
+
)
|
|
1762
1963
|
|
|
1763
1964
|
return result
|
|
1764
1965
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.268"
|
|
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
|