django-bulk-hooks 0.1.265__py3-none-any.whl → 0.1.267__py3-none-any.whl
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/manager.py +4 -6
- django_bulk_hooks/queryset.py +277 -166
- {django_bulk_hooks-0.1.265.dist-info → django_bulk_hooks-0.1.267.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.1.265.dist-info → django_bulk_hooks-0.1.267.dist-info}/RECORD +6 -6
- {django_bulk_hooks-0.1.265.dist-info → django_bulk_hooks-0.1.267.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.265.dist-info → django_bulk_hooks-0.1.267.dist-info}/WHEEL +0 -0
django_bulk_hooks/manager.py
CHANGED
|
@@ -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.
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -975,202 +975,308 @@ class HookQuerySetMixin:
|
|
|
975
975
|
|
|
976
976
|
@transaction.atomic
|
|
977
977
|
def bulk_update(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
978
|
+
if not objs:
|
|
979
|
+
return []
|
|
980
|
+
|
|
981
|
+
self._validate_objects(objs)
|
|
982
|
+
|
|
983
|
+
changed_fields = self._detect_changed_fields(objs)
|
|
984
|
+
is_mti = self._is_multi_table_inheritance()
|
|
985
|
+
hook_context, originals = self._init_hook_context(bypass_hooks, objs)
|
|
986
|
+
|
|
987
|
+
fields_set, auto_now_fields, custom_update_fields = self._prepare_update_fields(
|
|
988
|
+
changed_fields
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
self._apply_auto_now_fields(objs, auto_now_fields)
|
|
992
|
+
self._apply_custom_update_fields(objs, custom_update_fields, fields_set)
|
|
993
|
+
|
|
994
|
+
if is_mti:
|
|
995
|
+
return self._mti_bulk_update(objs, list(fields_set), **kwargs)
|
|
996
|
+
else:
|
|
997
|
+
return self._single_table_bulk_update(
|
|
998
|
+
objs, fields_set, auto_now_fields, **kwargs
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
def _apply_custom_update_fields(self, objs, custom_update_fields, fields_set):
|
|
978
1002
|
"""
|
|
979
|
-
|
|
980
|
-
|
|
1003
|
+
Call pre_save() for custom fields that require update handling
|
|
1004
|
+
(e.g., CurrentUserField) and update both the objects and the field set.
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
objs (list[Model]): The model instances being updated.
|
|
1008
|
+
custom_update_fields (list[Field]): Fields that define a pre_save() hook.
|
|
1009
|
+
fields_set (set[str]): The overall set of fields to update, mutated in place.
|
|
981
1010
|
"""
|
|
1011
|
+
if not custom_update_fields:
|
|
1012
|
+
return
|
|
1013
|
+
|
|
982
1014
|
model_cls = self.model
|
|
1015
|
+
pk_field_names = [f.name for f in model_cls._meta.pk_fields]
|
|
983
1016
|
|
|
984
|
-
|
|
985
|
-
|
|
1017
|
+
logger.debug(
|
|
1018
|
+
"Applying pre_save() on custom update fields: %s",
|
|
1019
|
+
[f.name for f in custom_update_fields],
|
|
1020
|
+
)
|
|
986
1021
|
|
|
987
|
-
|
|
1022
|
+
for obj in objs:
|
|
1023
|
+
for field in custom_update_fields:
|
|
1024
|
+
try:
|
|
1025
|
+
# Call pre_save with add=False (since this is an update)
|
|
1026
|
+
new_value = field.pre_save(obj, add=False)
|
|
1027
|
+
|
|
1028
|
+
# Only assign if pre_save returned something
|
|
1029
|
+
if new_value is not None:
|
|
1030
|
+
setattr(obj, field.name, new_value)
|
|
1031
|
+
|
|
1032
|
+
# Ensure this field is included in the update set
|
|
1033
|
+
if (
|
|
1034
|
+
field.name not in fields_set
|
|
1035
|
+
and field.name not in pk_field_names
|
|
1036
|
+
):
|
|
1037
|
+
fields_set.add(field.name)
|
|
1038
|
+
|
|
1039
|
+
logger.debug(
|
|
1040
|
+
"Custom field %s updated via pre_save() for object %s",
|
|
1041
|
+
field.name,
|
|
1042
|
+
obj.pk,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
except Exception as e:
|
|
1046
|
+
logger.warning(
|
|
1047
|
+
"Failed to call pre_save() on custom field %s for object %s: %s",
|
|
1048
|
+
field.name,
|
|
1049
|
+
getattr(obj, "pk", None),
|
|
1050
|
+
e,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
def _single_table_bulk_update(self, objs, fields_set, auto_now_fields, **kwargs):
|
|
1054
|
+
"""
|
|
1055
|
+
Perform bulk_update for single-table models, handling Django semantics
|
|
1056
|
+
for kwargs and setting a value map for hook execution.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
objs (list[Model]): The model instances being updated.
|
|
1060
|
+
fields_set (set[str]): The names of fields to update.
|
|
1061
|
+
auto_now_fields (list[str]): Names of auto_now fields included in update.
|
|
1062
|
+
**kwargs: Extra arguments (only Django-supported ones are passed through).
|
|
1063
|
+
|
|
1064
|
+
Returns:
|
|
1065
|
+
list[Model]: The updated model instances.
|
|
1066
|
+
"""
|
|
1067
|
+
# Strip out unsupported bulk_update kwargs
|
|
1068
|
+
django_kwargs = self._filter_django_kwargs(kwargs)
|
|
1069
|
+
|
|
1070
|
+
# Build a value map: {pk -> {field: raw_value}} for later hook use
|
|
1071
|
+
value_map = self._build_value_map(objs, fields_set, auto_now_fields)
|
|
1072
|
+
|
|
1073
|
+
if value_map:
|
|
1074
|
+
set_bulk_update_value_map(value_map)
|
|
1075
|
+
|
|
1076
|
+
try:
|
|
1077
|
+
logger.debug(
|
|
1078
|
+
"Calling Django bulk_update for %d objects on fields %s",
|
|
1079
|
+
len(objs),
|
|
1080
|
+
list(fields_set),
|
|
1081
|
+
)
|
|
1082
|
+
return super().bulk_update(objs, list(fields_set), **django_kwargs)
|
|
1083
|
+
finally:
|
|
1084
|
+
# Always clear thread-local state
|
|
1085
|
+
set_bulk_update_value_map(None)
|
|
1086
|
+
|
|
1087
|
+
def _filter_django_kwargs(self, kwargs):
|
|
1088
|
+
"""
|
|
1089
|
+
Remove unsupported arguments before passing to Django's bulk_update.
|
|
1090
|
+
"""
|
|
1091
|
+
unsupported = {
|
|
1092
|
+
"unique_fields",
|
|
1093
|
+
"update_conflicts",
|
|
1094
|
+
"update_fields",
|
|
1095
|
+
"ignore_conflicts",
|
|
1096
|
+
}
|
|
1097
|
+
passthrough = {}
|
|
1098
|
+
for k, v in kwargs.items():
|
|
1099
|
+
if k in unsupported:
|
|
1100
|
+
logger.warning(
|
|
1101
|
+
"Parameter '%s' is not supported by bulk_update. "
|
|
1102
|
+
"It is only available for bulk_create UPSERT operations.",
|
|
1103
|
+
k,
|
|
1104
|
+
)
|
|
1105
|
+
elif k not in {"bypass_hooks", "bypass_validation"}:
|
|
1106
|
+
passthrough[k] = v
|
|
1107
|
+
return passthrough
|
|
1108
|
+
|
|
1109
|
+
def _build_value_map(self, objs, fields_set, auto_now_fields):
|
|
1110
|
+
"""
|
|
1111
|
+
Build a mapping of {pk -> {field_name: raw_value}} for hook processing.
|
|
1112
|
+
|
|
1113
|
+
Expressions are not included; only concrete values assigned on the object.
|
|
1114
|
+
"""
|
|
1115
|
+
value_map = {}
|
|
1116
|
+
for obj in objs:
|
|
1117
|
+
if obj.pk is None:
|
|
1118
|
+
continue # skip unsaved objects
|
|
1119
|
+
field_values = {}
|
|
1120
|
+
for field_name in fields_set:
|
|
1121
|
+
value = getattr(obj, field_name)
|
|
1122
|
+
field_values[field_name] = value
|
|
1123
|
+
if field_name in auto_now_fields:
|
|
1124
|
+
logger.debug("Object %s %s=%s", obj.pk, field_name, value)
|
|
1125
|
+
if field_values:
|
|
1126
|
+
value_map[obj.pk] = field_values
|
|
1127
|
+
|
|
1128
|
+
logger.debug("Built value_map for %d objects", len(value_map))
|
|
1129
|
+
return value_map
|
|
1130
|
+
|
|
1131
|
+
def _validate_objects(self, objs):
|
|
1132
|
+
"""
|
|
1133
|
+
Validate that all objects are instances of this queryset's model
|
|
1134
|
+
and that they have primary keys (cannot bulk update unsaved objects).
|
|
1135
|
+
"""
|
|
1136
|
+
model_cls = self.model
|
|
1137
|
+
|
|
1138
|
+
# Type check
|
|
1139
|
+
invalid_types = {
|
|
1140
|
+
type(obj).__name__ for obj in objs if not isinstance(obj, model_cls)
|
|
1141
|
+
}
|
|
1142
|
+
if invalid_types:
|
|
988
1143
|
raise TypeError(
|
|
989
|
-
f"bulk_update expected instances of {model_cls.__name__},
|
|
1144
|
+
f"bulk_update expected instances of {model_cls.__name__}, "
|
|
1145
|
+
f"but got {invalid_types}"
|
|
990
1146
|
)
|
|
991
1147
|
|
|
992
|
-
#
|
|
993
|
-
|
|
994
|
-
|
|
1148
|
+
# Primary key check
|
|
1149
|
+
missing_pks = [obj for obj in objs if obj.pk is None]
|
|
1150
|
+
if missing_pks:
|
|
1151
|
+
raise ValueError(
|
|
1152
|
+
f"bulk_update cannot operate on unsaved {model_cls.__name__} instances. "
|
|
1153
|
+
f"{len(missing_pks)} object(s) have no primary key."
|
|
1154
|
+
)
|
|
995
1155
|
|
|
996
1156
|
logger.debug(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
f"DEBUG: bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)} changed_fields={changed_fields}"
|
|
1157
|
+
"Validated %d %s objects for bulk_update",
|
|
1158
|
+
len(objs),
|
|
1159
|
+
model_cls.__name__,
|
|
1001
1160
|
)
|
|
1002
1161
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
1007
|
-
is_mti = True
|
|
1008
|
-
break
|
|
1162
|
+
def _init_hook_context(self, bypass_hooks: bool, objs):
|
|
1163
|
+
"""
|
|
1164
|
+
Initialize the hook context for bulk_update.
|
|
1009
1165
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1166
|
+
Returns:
|
|
1167
|
+
(HookContext, list): The hook context and a placeholder list
|
|
1168
|
+
for 'originals', which can be populated later if needed for
|
|
1169
|
+
after_update hooks.
|
|
1170
|
+
"""
|
|
1171
|
+
model_cls = self.model
|
|
1172
|
+
|
|
1173
|
+
if bypass_hooks:
|
|
1174
|
+
logger.debug("bulk_update: hooks bypassed for %s", model_cls.__name__)
|
|
1016
1175
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1176
|
+
else:
|
|
1177
|
+
logger.debug("bulk_update: hooks enabled for %s", model_cls.__name__)
|
|
1178
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
1179
|
+
|
|
1180
|
+
# Keep `originals` aligned with objs to support later hook execution.
|
|
1181
|
+
originals = [None] * len(objs)
|
|
1020
1182
|
|
|
1021
|
-
|
|
1183
|
+
return ctx, originals
|
|
1184
|
+
|
|
1185
|
+
def _prepare_update_fields(self, changed_fields):
|
|
1186
|
+
"""
|
|
1187
|
+
Determine the final set of fields to update, including auto_now
|
|
1188
|
+
fields and custom fields that require pre_save() on updates.
|
|
1189
|
+
|
|
1190
|
+
Args:
|
|
1191
|
+
changed_fields (Iterable[str]): Fields detected as changed.
|
|
1192
|
+
|
|
1193
|
+
Returns:
|
|
1194
|
+
tuple:
|
|
1195
|
+
fields_set (set): All fields that should be updated.
|
|
1196
|
+
auto_now_fields (list[str]): Fields that require auto_now behavior.
|
|
1197
|
+
custom_update_fields (list[Field]): Fields with pre_save hooks to call.
|
|
1198
|
+
"""
|
|
1199
|
+
model_cls = self.model
|
|
1022
1200
|
fields_set = set(changed_fields)
|
|
1023
|
-
|
|
1024
|
-
|
|
1201
|
+
pk_field_names = [f.name for f in model_cls._meta.pk_fields]
|
|
1202
|
+
|
|
1025
1203
|
auto_now_fields = []
|
|
1026
|
-
custom_update_fields = []
|
|
1027
|
-
|
|
1028
|
-
f"Checking for auto_now and custom update fields in {model_cls.__name__}"
|
|
1029
|
-
)
|
|
1204
|
+
custom_update_fields = []
|
|
1205
|
+
|
|
1030
1206
|
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}")
|
|
1207
|
+
# Handle auto_now fields
|
|
1208
|
+
if getattr(field, "auto_now", False):
|
|
1036
1209
|
if field.name not in fields_set and field.name not in pk_field_names:
|
|
1037
1210
|
fields_set.add(field.name)
|
|
1038
|
-
if field.name != field.attname:
|
|
1211
|
+
if field.name != field.attname: # handle attname vs name
|
|
1039
1212
|
fields_set.add(field.attname)
|
|
1040
1213
|
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)
|
|
1214
|
+
logger.debug("Added auto_now field %s to update set", field.name)
|
|
1215
|
+
|
|
1216
|
+
# Skip auto_now_add (only applies at creation time)
|
|
1217
|
+
elif getattr(field, "auto_now_add", False):
|
|
1218
|
+
continue
|
|
1219
|
+
|
|
1220
|
+
# Handle custom pre_save fields
|
|
1053
1221
|
elif hasattr(field, "pre_save"):
|
|
1054
|
-
# Only call pre_save on fields that aren't already being updated
|
|
1055
1222
|
if field.name not in fields_set and field.name not in pk_field_names:
|
|
1056
1223
|
custom_update_fields.append(field)
|
|
1057
|
-
logger.debug(
|
|
1058
|
-
|
|
1224
|
+
logger.debug(
|
|
1225
|
+
"Marked custom field %s for pre_save update", field.name
|
|
1226
|
+
)
|
|
1059
1227
|
|
|
1060
|
-
logger.debug(
|
|
1061
|
-
|
|
1228
|
+
logger.debug(
|
|
1229
|
+
"Prepared update fields: fields_set=%s, auto_now_fields=%s, custom_update_fields=%s",
|
|
1230
|
+
fields_set,
|
|
1231
|
+
auto_now_fields,
|
|
1232
|
+
[f.name for f in custom_update_fields],
|
|
1233
|
+
)
|
|
1062
1234
|
|
|
1063
|
-
|
|
1064
|
-
if auto_now_fields:
|
|
1065
|
-
from django.utils import timezone
|
|
1235
|
+
return fields_set, auto_now_fields, custom_update_fields
|
|
1066
1236
|
|
|
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
|
-
)
|
|
1237
|
+
def _apply_auto_now_fields(self, objs, auto_now_fields):
|
|
1238
|
+
"""
|
|
1239
|
+
Apply the current timestamp to all auto_now fields on each object.
|
|
1080
1240
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
f"DEBUG: Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}"
|
|
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
|
-
)
|
|
1241
|
+
Args:
|
|
1242
|
+
objs (list[Model]): The model instances being updated.
|
|
1243
|
+
auto_now_fields (list[str]): Field names that require auto_now behavior.
|
|
1244
|
+
"""
|
|
1245
|
+
if not auto_now_fields:
|
|
1246
|
+
return
|
|
1116
1247
|
|
|
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
|
-
for k, v in kwargs.items():
|
|
1126
|
-
if k in unsupported_params:
|
|
1127
|
-
logger.warning(
|
|
1128
|
-
f"Parameter '{k}' is not supported by bulk_update. "
|
|
1129
|
-
f"This parameter is only available in bulk_create for UPSERT operations."
|
|
1130
|
-
)
|
|
1131
|
-
print(f"WARNING: Parameter '{k}' is not supported by bulk_update")
|
|
1132
|
-
elif k not in ["bypass_hooks", "bypass_validation"]:
|
|
1133
|
-
django_kwargs[k] = v
|
|
1134
|
-
logger.debug("Calling Django bulk_update")
|
|
1135
|
-
print("DEBUG: Calling Django bulk_update")
|
|
1136
|
-
# Build a per-object concrete value map to avoid leaking expressions into hooks
|
|
1137
|
-
value_map = {}
|
|
1138
|
-
logger.debug(
|
|
1139
|
-
f"Building value map for {len(objs)} objects with fields: {list(fields_set)}"
|
|
1140
|
-
)
|
|
1141
|
-
for obj in objs:
|
|
1142
|
-
if obj.pk is None:
|
|
1143
|
-
continue
|
|
1144
|
-
field_values = {}
|
|
1145
|
-
for field_name in fields_set:
|
|
1146
|
-
# Capture raw values assigned on the object (not expressions)
|
|
1147
|
-
field_values[field_name] = getattr(obj, field_name)
|
|
1148
|
-
if field_name in auto_now_fields:
|
|
1149
|
-
logger.debug(
|
|
1150
|
-
f"Object {obj.pk} {field_name}: {field_values[field_name]}"
|
|
1151
|
-
)
|
|
1152
|
-
if field_values:
|
|
1153
|
-
value_map[obj.pk] = field_values
|
|
1154
|
-
|
|
1155
|
-
# Make the value map available to the subsequent update() call
|
|
1156
|
-
if value_map:
|
|
1157
|
-
set_bulk_update_value_map(value_map)
|
|
1158
|
-
|
|
1159
|
-
try:
|
|
1160
|
-
result = super().bulk_update(objs, list(fields_set), **django_kwargs)
|
|
1161
|
-
finally:
|
|
1162
|
-
# Always clear after the internal update() path finishes
|
|
1163
|
-
set_bulk_update_value_map(None)
|
|
1164
|
-
logger.debug(f"Django bulk_update done: {result}")
|
|
1165
|
-
|
|
1166
|
-
# Note: We don't run AFTER_UPDATE hooks here to prevent double execution
|
|
1167
|
-
# The update() method will handle all hook execution based on thread-local state
|
|
1168
|
-
if not bypass_hooks:
|
|
1169
|
-
logger.debug("bulk_update: skipping AFTER_UPDATE (update() will handle)")
|
|
1170
|
-
else:
|
|
1171
|
-
logger.debug("bulk_update: hooks bypassed")
|
|
1248
|
+
from django.utils import timezone
|
|
1172
1249
|
|
|
1173
|
-
|
|
1250
|
+
current_time = timezone.now()
|
|
1251
|
+
|
|
1252
|
+
logger.debug(
|
|
1253
|
+
"Setting auto_now fields %s to %s for %d objects",
|
|
1254
|
+
auto_now_fields,
|
|
1255
|
+
current_time,
|
|
1256
|
+
len(objs),
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
for obj in objs:
|
|
1260
|
+
for field_name in auto_now_fields:
|
|
1261
|
+
setattr(obj, field_name, current_time)
|
|
1262
|
+
|
|
1263
|
+
def _is_multi_table_inheritance(self) -> bool:
|
|
1264
|
+
"""
|
|
1265
|
+
Determine whether this model uses multi-table inheritance (MTI).
|
|
1266
|
+
Returns True if the model has any concrete parent models other than itself.
|
|
1267
|
+
"""
|
|
1268
|
+
model_cls = self.model
|
|
1269
|
+
for parent in model_cls._meta.all_parents:
|
|
1270
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
1271
|
+
logger.debug(
|
|
1272
|
+
"%s detected as MTI model (parent: %s)",
|
|
1273
|
+
model_cls.__name__,
|
|
1274
|
+
parent.__name__,
|
|
1275
|
+
)
|
|
1276
|
+
return True
|
|
1277
|
+
|
|
1278
|
+
logger.debug("%s is not an MTI model", model_cls.__name__)
|
|
1279
|
+
return False
|
|
1174
1280
|
|
|
1175
1281
|
def _detect_modified_fields(self, new_instances, original_instances):
|
|
1176
1282
|
"""
|
|
@@ -1491,7 +1597,12 @@ class HookQuerySetMixin:
|
|
|
1491
1597
|
inheritance_chain = self._get_inheritance_chain()
|
|
1492
1598
|
|
|
1493
1599
|
# Remove custom hook kwargs and unsupported parameters before passing to Django internals
|
|
1494
|
-
unsupported_params = [
|
|
1600
|
+
unsupported_params = [
|
|
1601
|
+
"unique_fields",
|
|
1602
|
+
"update_conflicts",
|
|
1603
|
+
"update_fields",
|
|
1604
|
+
"ignore_conflicts",
|
|
1605
|
+
]
|
|
1495
1606
|
django_kwargs = {}
|
|
1496
1607
|
for k, v in kwargs.items():
|
|
1497
1608
|
if k in unsupported_params:
|
|
@@ -6,12 +6,12 @@ django_bulk_hooks/decorators.py,sha256=k70-BzWwS3wZu_uph5B5qXd6YpwXLQ9hMpOzPUy6i
|
|
|
6
6
|
django_bulk_hooks/engine.py,sha256=M3b7Rcb65PYAZTLfWrIRi99BUBPgSLCryL3MSjMVlfQ,2663
|
|
7
7
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
8
|
django_bulk_hooks/handler.py,sha256=Bx-W6yyiciKMyy-BRxUt3CmRPCrX9_LhQgU-5LaJTjg,6019
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=3jNWL-EkvGScsliNc7mW-ozQCG6HyaEevI1u1BFS4AA,3836
|
|
10
10
|
django_bulk_hooks/models.py,sha256=WtSfc4GBOG_oOt8n37cVvid0MtFIGze9JYKSixil2y0,4370
|
|
11
11
|
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=xp8Xa2yxc278A11Voy0sx5eYn6O5p2y1nzuEB57q-Tg,84694
|
|
13
13
|
django_bulk_hooks/registry.py,sha256=GRUTGVQEO2sdkC9OaZ9Q3U7mM-3Ix83uTyvrlTtpatw,1317
|
|
14
|
-
django_bulk_hooks-0.1.
|
|
15
|
-
django_bulk_hooks-0.1.
|
|
16
|
-
django_bulk_hooks-0.1.
|
|
17
|
-
django_bulk_hooks-0.1.
|
|
14
|
+
django_bulk_hooks-0.1.267.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.267.dist-info/METADATA,sha256=WqAB7A5yKuD9PKX9bte-a3kk7MkhrUG8AQ_-xqoNJaE,9115
|
|
16
|
+
django_bulk_hooks-0.1.267.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
django_bulk_hooks-0.1.267.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|