django-bulk-hooks 0.1.173__tar.gz → 0.1.176__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.173 → django_bulk_hooks-0.1.176}/PKG-INFO +3 -3
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/manager.py +7 -5
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/queryset.py +112 -151
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/LICENSE +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/README.md +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.173 → django_bulk_hooks-0.1.176}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.176
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
-
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: django,bulk,hooks
|
|
8
7
|
Author: Konrad Beck
|
|
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Requires-Dist: Django (>=4.0)
|
|
16
|
+
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -58,12 +58,12 @@ class BulkHookManager(models.Manager):
|
|
|
58
58
|
"""
|
|
59
59
|
import inspect
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
queryset = self.get_queryset()
|
|
62
62
|
|
|
63
63
|
# Check if this is our HookQuerySet or a different QuerySet
|
|
64
64
|
if (
|
|
65
|
-
hasattr(
|
|
66
|
-
and "bypass_hooks" in inspect.signature(
|
|
65
|
+
hasattr(queryset, "bulk_create")
|
|
66
|
+
and "bypass_hooks" in inspect.signature(queryset.bulk_create).parameters
|
|
67
67
|
):
|
|
68
68
|
# Our HookQuerySet - pass all parameters
|
|
69
69
|
kwargs = {
|
|
@@ -73,13 +73,15 @@ class BulkHookManager(models.Manager):
|
|
|
73
73
|
"update_fields": update_fields,
|
|
74
74
|
"unique_fields": unique_fields,
|
|
75
75
|
}
|
|
76
|
-
return
|
|
76
|
+
return queryset.bulk_create(
|
|
77
77
|
objs,
|
|
78
78
|
bypass_hooks=bypass_hooks,
|
|
79
79
|
bypass_validation=bypass_validation,
|
|
80
80
|
**kwargs,
|
|
81
81
|
)
|
|
82
82
|
else:
|
|
83
|
+
print("Different QuerySet")
|
|
84
|
+
|
|
83
85
|
# Different QuerySet - only pass standard parameters
|
|
84
86
|
kwargs = {
|
|
85
87
|
"batch_size": batch_size,
|
|
@@ -88,7 +90,7 @@ class BulkHookManager(models.Manager):
|
|
|
88
90
|
"update_fields": update_fields,
|
|
89
91
|
"unique_fields": unique_fields,
|
|
90
92
|
}
|
|
91
|
-
return
|
|
93
|
+
return queryset.bulk_create(objs, **kwargs)
|
|
92
94
|
|
|
93
95
|
def bulk_delete(
|
|
94
96
|
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
@@ -24,8 +24,23 @@ class HookQuerySet(models.QuerySet):
|
|
|
24
24
|
objs = list(self)
|
|
25
25
|
if not objs:
|
|
26
26
|
return 0
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
model_cls = self.model
|
|
29
|
+
ctx = HookContext(model_cls)
|
|
30
|
+
|
|
31
|
+
# Run validation hooks first
|
|
32
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
33
|
+
|
|
34
|
+
# Then run business logic hooks
|
|
35
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
36
|
+
|
|
37
|
+
# Use Django's standard delete() method
|
|
38
|
+
result = super().delete()
|
|
39
|
+
|
|
40
|
+
# Run AFTER_DELETE hooks
|
|
41
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
42
|
+
|
|
43
|
+
return result
|
|
29
44
|
|
|
30
45
|
@transaction.atomic
|
|
31
46
|
def update(self, **kwargs):
|
|
@@ -38,7 +53,9 @@ class HookQuerySet(models.QuerySet):
|
|
|
38
53
|
|
|
39
54
|
# Load originals for hook comparison and ensure they match the order of instances
|
|
40
55
|
# Use the base manager to avoid recursion
|
|
41
|
-
original_map = {
|
|
56
|
+
original_map = {
|
|
57
|
+
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
58
|
+
}
|
|
42
59
|
originals = [original_map.get(obj.pk) for obj in instances]
|
|
43
60
|
|
|
44
61
|
# Apply field updates to instances
|
|
@@ -92,6 +109,14 @@ class HookQuerySet(models.QuerySet):
|
|
|
92
109
|
# trickier so it's not done yet.
|
|
93
110
|
if batch_size is not None and batch_size <= 0:
|
|
94
111
|
raise ValueError("Batch size must be a positive integer.")
|
|
112
|
+
|
|
113
|
+
if not objs:
|
|
114
|
+
return objs
|
|
115
|
+
|
|
116
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
117
|
+
raise TypeError(
|
|
118
|
+
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
119
|
+
)
|
|
95
120
|
|
|
96
121
|
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
97
122
|
# This follows Django's approach: check that the parents share the same concrete model
|
|
@@ -104,55 +129,35 @@ class HookQuerySet(models.QuerySet):
|
|
|
104
129
|
is_mti = True
|
|
105
130
|
break
|
|
106
131
|
|
|
107
|
-
if not objs:
|
|
108
|
-
return objs
|
|
109
|
-
|
|
110
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
111
|
-
raise TypeError(
|
|
112
|
-
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
113
|
-
)
|
|
114
|
-
|
|
115
132
|
# Fire hooks before DB ops
|
|
116
133
|
if not bypass_hooks:
|
|
117
|
-
print(f"DEBUG: Firing BEFORE_CREATE hooks for {model_cls}")
|
|
118
|
-
print(f"DEBUG: Number of objects: {len(objs)}")
|
|
119
|
-
print(f"DEBUG: Object types: {[type(obj) for obj in objs]}")
|
|
120
|
-
print(f"DEBUG: QuerySet type: {type(self)}")
|
|
121
|
-
print(f"DEBUG: Is this HookQuerySet? {isinstance(self, HookQuerySet)}")
|
|
122
134
|
ctx = HookContext(model_cls)
|
|
123
135
|
if not bypass_validation:
|
|
124
|
-
print(f"DEBUG: Running VALIDATE_CREATE hooks")
|
|
125
136
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
126
|
-
print(f"DEBUG: Running BEFORE_CREATE hooks")
|
|
127
137
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
128
|
-
else:
|
|
129
|
-
print(f"DEBUG: Skipping hooks due to bypass_hooks=True for {model_cls}")
|
|
130
138
|
|
|
131
139
|
# For MTI models, we need to handle them specially
|
|
132
140
|
if is_mti:
|
|
133
141
|
# Use our MTI-specific logic
|
|
134
142
|
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
135
143
|
mti_kwargs = {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
"batch_size": batch_size,
|
|
145
|
+
"ignore_conflicts": ignore_conflicts,
|
|
146
|
+
"update_conflicts": update_conflicts,
|
|
147
|
+
"update_fields": update_fields,
|
|
148
|
+
"unique_fields": unique_fields,
|
|
141
149
|
}
|
|
142
150
|
# Remove custom hook kwargs if present in self.bulk_create signature
|
|
143
151
|
result = self._mti_bulk_create(
|
|
144
152
|
objs,
|
|
145
|
-
|
|
153
|
+
mti_kwargs,
|
|
146
154
|
)
|
|
147
155
|
else:
|
|
148
156
|
# For single-table models, use Django's built-in bulk_create
|
|
149
157
|
# but we need to call it on the base manager to avoid recursion
|
|
150
158
|
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
151
159
|
|
|
152
|
-
|
|
153
|
-
from django.db.models import QuerySet
|
|
154
|
-
original_qs = QuerySet(model_cls, using=self.db)
|
|
155
|
-
result = original_qs.bulk_create(
|
|
160
|
+
result = super().bulk_create(
|
|
156
161
|
objs,
|
|
157
162
|
batch_size=batch_size,
|
|
158
163
|
ignore_conflicts=ignore_conflicts,
|
|
@@ -163,37 +168,19 @@ class HookQuerySet(models.QuerySet):
|
|
|
163
168
|
|
|
164
169
|
# Fire AFTER_CREATE hooks
|
|
165
170
|
if not bypass_hooks:
|
|
166
|
-
print(f"DEBUG: Firing AFTER_CREATE hooks for {model_cls}")
|
|
167
|
-
print(f"DEBUG: Number of objects: {len(objs)}")
|
|
168
|
-
print(f"DEBUG: QuerySet type: {type(self)}")
|
|
169
|
-
print(f"DEBUG: Is this HookQuerySet? {isinstance(self, HookQuerySet)}")
|
|
170
171
|
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
171
|
-
else:
|
|
172
|
-
print(f"DEBUG: Skipping AFTER_CREATE hooks due to bypass_hooks=True for {model_cls}")
|
|
173
172
|
|
|
174
173
|
return result
|
|
175
174
|
|
|
176
175
|
@transaction.atomic
|
|
177
|
-
def bulk_update(
|
|
176
|
+
def bulk_update(
|
|
177
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
178
|
+
):
|
|
178
179
|
"""
|
|
179
180
|
Bulk update objects in the database.
|
|
180
181
|
"""
|
|
181
|
-
import inspect
|
|
182
|
-
print(f"DEBUG: QuerySet.bulk_update called with:")
|
|
183
|
-
print(f" - self: {type(self)}")
|
|
184
|
-
print(f" - objs: {type(objs)}")
|
|
185
|
-
print(f" - fields: {fields}")
|
|
186
|
-
print(f" - bypass_hooks: {bypass_hooks}")
|
|
187
|
-
print(f" - bypass_validation: {bypass_validation}")
|
|
188
|
-
print(f" - kwargs: {kwargs}")
|
|
189
|
-
print(f"DEBUG: Method signature: {inspect.signature(self.bulk_update)}")
|
|
190
|
-
|
|
191
182
|
model_cls = self.model
|
|
192
|
-
|
|
193
|
-
print(f"DEBUG: bypass_hooks value: {bypass_hooks}")
|
|
194
|
-
print(f"DEBUG: QuerySet type: {type(self)}")
|
|
195
|
-
print(f"DEBUG: Is this HookQuerySet? {isinstance(self, HookQuerySet)}")
|
|
196
|
-
|
|
183
|
+
|
|
197
184
|
if not objs:
|
|
198
185
|
return []
|
|
199
186
|
|
|
@@ -207,7 +194,9 @@ class HookQuerySet(models.QuerySet):
|
|
|
207
194
|
# Use the base manager to avoid recursion
|
|
208
195
|
original_map = {
|
|
209
196
|
obj.pk: obj
|
|
210
|
-
for obj in model_cls._base_manager.filter(
|
|
197
|
+
for obj in model_cls._base_manager.filter(
|
|
198
|
+
pk__in=[obj.pk for obj in objs]
|
|
199
|
+
)
|
|
211
200
|
}
|
|
212
201
|
originals = [original_map.get(obj.pk) for obj in objs]
|
|
213
202
|
|
|
@@ -219,12 +208,7 @@ class HookQuerySet(models.QuerySet):
|
|
|
219
208
|
|
|
220
209
|
# Then run business logic hooks
|
|
221
210
|
if not bypass_hooks:
|
|
222
|
-
print(f"DEBUG: Firing BEFORE_UPDATE hooks for {model_cls}")
|
|
223
|
-
print(f"DEBUG: Number of objects: {len(objs)}")
|
|
224
|
-
print(f"DEBUG: Object types: {[type(obj) for obj in objs]}")
|
|
225
211
|
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
226
|
-
else:
|
|
227
|
-
print(f"DEBUG: Skipping hooks due to bypass_hooks=True for {model_cls}")
|
|
228
212
|
|
|
229
213
|
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
230
214
|
modified_fields = self._detect_modified_fields(objs, originals)
|
|
@@ -234,55 +218,15 @@ class HookQuerySet(models.QuerySet):
|
|
|
234
218
|
fields_set.update(modified_fields)
|
|
235
219
|
fields = list(fields_set)
|
|
236
220
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
super().bulk_update(chunk, fields, **django_kwargs)
|
|
221
|
+
django_kwargs = {
|
|
222
|
+
k: v
|
|
223
|
+
for k, v in kwargs.items()
|
|
224
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
225
|
+
}
|
|
226
|
+
super().bulk_update(objs, fields, **django_kwargs)
|
|
244
227
|
|
|
245
228
|
if not bypass_hooks:
|
|
246
|
-
print(f"DEBUG: Firing AFTER_UPDATE hooks for {model_cls}")
|
|
247
|
-
print(f"DEBUG: Number of objects: {len(objs)}")
|
|
248
229
|
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
249
|
-
else:
|
|
250
|
-
print(f"DEBUG: Skipping AFTER_UPDATE hooks due to bypass_hooks=True for {model_cls}")
|
|
251
|
-
|
|
252
|
-
return objs
|
|
253
|
-
|
|
254
|
-
@transaction.atomic
|
|
255
|
-
def bulk_delete(
|
|
256
|
-
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
257
|
-
):
|
|
258
|
-
if not objs:
|
|
259
|
-
return []
|
|
260
|
-
|
|
261
|
-
model_cls = self.model
|
|
262
|
-
|
|
263
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
264
|
-
raise TypeError(
|
|
265
|
-
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
ctx = HookContext(model_cls)
|
|
269
|
-
|
|
270
|
-
if not bypass_hooks:
|
|
271
|
-
# Run validation hooks first
|
|
272
|
-
if not bypass_validation:
|
|
273
|
-
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
274
|
-
|
|
275
|
-
# Then run business logic hooks
|
|
276
|
-
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
277
|
-
|
|
278
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
279
|
-
|
|
280
|
-
# Call the base QuerySet implementation to avoid recursion
|
|
281
|
-
# The hooks have already been fired above, so we don't need them again
|
|
282
|
-
super().bulk_delete(objs, batch_size=batch_size)
|
|
283
|
-
|
|
284
|
-
if not bypass_hooks:
|
|
285
|
-
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
286
230
|
|
|
287
231
|
return objs
|
|
288
232
|
|
|
@@ -350,7 +294,11 @@ class HookQuerySet(models.QuerySet):
|
|
|
350
294
|
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
351
295
|
"""
|
|
352
296
|
# Remove custom hook kwargs before passing to Django internals
|
|
353
|
-
django_kwargs = {
|
|
297
|
+
django_kwargs = {
|
|
298
|
+
k: v
|
|
299
|
+
for k, v in kwargs.items()
|
|
300
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
301
|
+
}
|
|
354
302
|
if inheritance_chain is None:
|
|
355
303
|
inheritance_chain = self._get_inheritance_chain()
|
|
356
304
|
|
|
@@ -383,9 +331,9 @@ class HookQuerySet(models.QuerySet):
|
|
|
383
331
|
|
|
384
332
|
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
385
333
|
# Get bypass_hooks from kwargs
|
|
386
|
-
bypass_hooks = kwargs.get(
|
|
387
|
-
bypass_validation = kwargs.get(
|
|
388
|
-
|
|
334
|
+
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
335
|
+
bypass_validation = kwargs.get("bypass_validation", False)
|
|
336
|
+
|
|
389
337
|
for obj in batch:
|
|
390
338
|
parent_instances = {}
|
|
391
339
|
current_parent = None
|
|
@@ -393,32 +341,35 @@ class HookQuerySet(models.QuerySet):
|
|
|
393
341
|
parent_obj = self._create_parent_instance(
|
|
394
342
|
obj, model_class, current_parent
|
|
395
343
|
)
|
|
396
|
-
|
|
344
|
+
|
|
397
345
|
# Fire parent hooks if not bypassed
|
|
398
346
|
if not bypass_hooks:
|
|
399
347
|
ctx = HookContext(model_class)
|
|
400
348
|
if not bypass_validation:
|
|
401
349
|
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
402
350
|
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
403
|
-
|
|
351
|
+
|
|
404
352
|
# Use Django's base manager to create the object and get PKs back
|
|
405
353
|
# This bypasses hooks and the MTI exception
|
|
406
354
|
field_values = {
|
|
407
355
|
field.name: getattr(parent_obj, field.name)
|
|
408
356
|
for field in model_class._meta.local_fields
|
|
409
|
-
if hasattr(parent_obj, field.name)
|
|
357
|
+
if hasattr(parent_obj, field.name)
|
|
358
|
+
and getattr(parent_obj, field.name) is not None
|
|
410
359
|
}
|
|
411
|
-
created_obj = model_class._base_manager.using(self.db).create(
|
|
412
|
-
|
|
360
|
+
created_obj = model_class._base_manager.using(self.db).create(
|
|
361
|
+
**field_values
|
|
362
|
+
)
|
|
363
|
+
|
|
413
364
|
# Update the parent_obj with the created object's PK
|
|
414
365
|
parent_obj.pk = created_obj.pk
|
|
415
366
|
parent_obj._state.adding = False
|
|
416
367
|
parent_obj._state.db = self.db
|
|
417
|
-
|
|
368
|
+
|
|
418
369
|
# Fire AFTER_CREATE hooks for parent
|
|
419
370
|
if not bypass_hooks:
|
|
420
371
|
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
421
|
-
|
|
372
|
+
|
|
422
373
|
parent_instances[model_class] = parent_obj
|
|
423
374
|
current_parent = parent_obj
|
|
424
375
|
parent_objects_map[id(obj)] = parent_instances
|
|
@@ -436,10 +387,10 @@ class HookQuerySet(models.QuerySet):
|
|
|
436
387
|
if all_child_objects:
|
|
437
388
|
# Get the base manager's queryset
|
|
438
389
|
base_qs = child_model._base_manager.using(self.db)
|
|
439
|
-
|
|
390
|
+
|
|
440
391
|
# Use Django's exact approach: call _prepare_for_bulk_create then partition
|
|
441
392
|
base_qs._prepare_for_bulk_create(all_child_objects)
|
|
442
|
-
|
|
393
|
+
|
|
443
394
|
# Implement our own partition since itertools.partition might not be available
|
|
444
395
|
objs_without_pk, objs_with_pk = [], []
|
|
445
396
|
for obj in all_child_objects:
|
|
@@ -447,44 +398,50 @@ class HookQuerySet(models.QuerySet):
|
|
|
447
398
|
objs_with_pk.append(obj)
|
|
448
399
|
else:
|
|
449
400
|
objs_without_pk.append(obj)
|
|
450
|
-
|
|
401
|
+
|
|
451
402
|
# Use Django's internal _batched_insert method
|
|
452
403
|
opts = child_model._meta
|
|
453
404
|
# For child models in MTI, we need to include the foreign key to the parent
|
|
454
405
|
# but exclude the primary key since it's inherited
|
|
455
|
-
|
|
406
|
+
|
|
456
407
|
# Include all local fields except generated ones
|
|
457
408
|
# We need to include the foreign key to the parent (business_ptr)
|
|
458
409
|
fields = [f for f in opts.local_fields if not f.generated]
|
|
459
|
-
|
|
410
|
+
|
|
460
411
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
412
|
+
if objs_with_pk:
|
|
413
|
+
returned_columns = base_qs._batched_insert(
|
|
414
|
+
objs_with_pk,
|
|
415
|
+
fields,
|
|
416
|
+
batch_size=len(objs_with_pk), # Use actual batch size
|
|
417
|
+
)
|
|
418
|
+
for obj_with_pk, results in zip(objs_with_pk, returned_columns):
|
|
419
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
420
|
+
if field != opts.pk:
|
|
421
|
+
setattr(obj_with_pk, field.attname, result)
|
|
422
|
+
for obj_with_pk in objs_with_pk:
|
|
423
|
+
obj_with_pk._state.adding = False
|
|
424
|
+
obj_with_pk._state.db = self.db
|
|
425
|
+
|
|
426
|
+
if objs_without_pk:
|
|
427
|
+
# For objects without PK, we still need to exclude primary key fields
|
|
428
|
+
fields = [
|
|
429
|
+
f
|
|
430
|
+
for f in fields
|
|
431
|
+
if not isinstance(f, AutoField) and not f.primary_key
|
|
432
|
+
]
|
|
433
|
+
returned_columns = base_qs._batched_insert(
|
|
434
|
+
objs_without_pk,
|
|
435
|
+
fields,
|
|
436
|
+
batch_size=len(objs_without_pk), # Use actual batch size
|
|
437
|
+
)
|
|
438
|
+
for obj_without_pk, results in zip(
|
|
439
|
+
objs_without_pk, returned_columns
|
|
440
|
+
):
|
|
441
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
442
|
+
setattr(obj_without_pk, field.attname, result)
|
|
443
|
+
obj_without_pk._state.adding = False
|
|
444
|
+
obj_without_pk._state.db = self.db
|
|
488
445
|
|
|
489
446
|
# Step 3: Update original objects with generated PKs and state
|
|
490
447
|
pk_field_name = child_model._meta.pk.name
|
|
@@ -537,15 +494,19 @@ class HookQuerySet(models.QuerySet):
|
|
|
537
494
|
value = getattr(source_obj, field.name, None)
|
|
538
495
|
if value is not None:
|
|
539
496
|
setattr(child_obj, field.name, value)
|
|
540
|
-
|
|
497
|
+
|
|
541
498
|
# Set parent links for MTI
|
|
542
499
|
for parent_model, parent_instance in parent_instances.items():
|
|
543
500
|
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
544
501
|
if parent_link:
|
|
545
502
|
# Set both the foreign key value (the ID) and the object reference
|
|
546
503
|
# This follows Django's pattern in _set_pk_val
|
|
547
|
-
setattr(
|
|
548
|
-
|
|
504
|
+
setattr(
|
|
505
|
+
child_obj, parent_link.attname, parent_instance.pk
|
|
506
|
+
) # Set the foreign key value
|
|
507
|
+
setattr(
|
|
508
|
+
child_obj, parent_link.name, parent_instance
|
|
509
|
+
) # Set the object reference
|
|
549
510
|
|
|
550
511
|
# Handle auto_now_add and auto_now fields like Django does
|
|
551
512
|
for field in child_model._meta.local_fields:
|
|
@@ -558,4 +519,4 @@ class HookQuerySet(models.QuerySet):
|
|
|
558
519
|
elif hasattr(field, "auto_now") and field.auto_now:
|
|
559
520
|
field.pre_save(child_obj, add=True)
|
|
560
521
|
|
|
561
|
-
return child_obj
|
|
522
|
+
return child_obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.176"
|
|
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
|