django-bulk-hooks 0.1.162__tar.gz → 0.1.163__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.162 → django_bulk_hooks-0.1.163}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/queryset.py +540 -524
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/LICENSE +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/README.md +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.162 → django_bulk_hooks-0.1.163}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,524 +1,540 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
from django.db.models import AutoField
|
|
3
|
-
|
|
4
|
-
from django_bulk_hooks import engine
|
|
5
|
-
from django_bulk_hooks.constants import (
|
|
6
|
-
AFTER_CREATE,
|
|
7
|
-
AFTER_DELETE,
|
|
8
|
-
AFTER_UPDATE,
|
|
9
|
-
BEFORE_CREATE,
|
|
10
|
-
BEFORE_DELETE,
|
|
11
|
-
BEFORE_UPDATE,
|
|
12
|
-
VALIDATE_CREATE,
|
|
13
|
-
VALIDATE_DELETE,
|
|
14
|
-
VALIDATE_UPDATE,
|
|
15
|
-
)
|
|
16
|
-
from django_bulk_hooks.context import HookContext
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class HookQuerySet(models.QuerySet):
|
|
20
|
-
CHUNK_SIZE = 200
|
|
21
|
-
|
|
22
|
-
@transaction.atomic
|
|
23
|
-
def delete(self):
|
|
24
|
-
objs = list(self)
|
|
25
|
-
if not objs:
|
|
26
|
-
return 0
|
|
27
|
-
return self.model.objects.bulk_delete(objs)
|
|
28
|
-
|
|
29
|
-
@transaction.atomic
|
|
30
|
-
def update(self, **kwargs):
|
|
31
|
-
instances = list(self)
|
|
32
|
-
if not instances:
|
|
33
|
-
return 0
|
|
34
|
-
|
|
35
|
-
model_cls = self.model
|
|
36
|
-
pks = [obj.pk for obj in instances]
|
|
37
|
-
|
|
38
|
-
# Load originals for hook comparison and ensure they match the order of instances
|
|
39
|
-
original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
|
|
40
|
-
originals = [original_map.get(obj.pk) for obj in instances]
|
|
41
|
-
|
|
42
|
-
# Apply field updates to instances
|
|
43
|
-
for obj in instances:
|
|
44
|
-
for field, value in kwargs.items():
|
|
45
|
-
setattr(obj, field, value)
|
|
46
|
-
|
|
47
|
-
# Run BEFORE_UPDATE hooks
|
|
48
|
-
ctx = HookContext(model_cls)
|
|
49
|
-
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
50
|
-
|
|
51
|
-
# Use Django's built-in update logic directly
|
|
52
|
-
queryset = self.model.objects.filter(pk__in=pks)
|
|
53
|
-
update_count = queryset.update(**kwargs)
|
|
54
|
-
|
|
55
|
-
# Run AFTER_UPDATE hooks
|
|
56
|
-
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
57
|
-
|
|
58
|
-
return update_count
|
|
59
|
-
|
|
60
|
-
@transaction.atomic
|
|
61
|
-
def bulk_create(
|
|
62
|
-
self,
|
|
63
|
-
objs,
|
|
64
|
-
batch_size=None,
|
|
65
|
-
ignore_conflicts=False,
|
|
66
|
-
update_conflicts=False,
|
|
67
|
-
update_fields=None,
|
|
68
|
-
unique_fields=None,
|
|
69
|
-
bypass_hooks=False,
|
|
70
|
-
bypass_validation=False,
|
|
71
|
-
):
|
|
72
|
-
"""
|
|
73
|
-
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
74
|
-
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
75
|
-
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
76
|
-
"""
|
|
77
|
-
model_cls = self.model
|
|
78
|
-
|
|
79
|
-
# When you bulk insert you don't get the primary keys back (if it's an
|
|
80
|
-
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
81
|
-
# you can't insert into the child tables which references this. There
|
|
82
|
-
# are two workarounds:
|
|
83
|
-
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
84
|
-
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
85
|
-
# tables to get the primary keys back and then doing a single bulk
|
|
86
|
-
# insert into the childmost table.
|
|
87
|
-
# We currently set the primary keys on the objects when using
|
|
88
|
-
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
89
|
-
# Oracle as well, but the semantics for extracting the primary keys is
|
|
90
|
-
# trickier so it's not done yet.
|
|
91
|
-
if batch_size is not None and batch_size <= 0:
|
|
92
|
-
raise ValueError("Batch size must be a positive integer.")
|
|
93
|
-
|
|
94
|
-
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
95
|
-
# This follows Django's approach: check that the parents share the same concrete model
|
|
96
|
-
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
97
|
-
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
98
|
-
# identify that case as involving multiple tables.
|
|
99
|
-
is_mti = False
|
|
100
|
-
for parent in model_cls._meta.all_parents:
|
|
101
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
102
|
-
is_mti = True
|
|
103
|
-
break
|
|
104
|
-
|
|
105
|
-
if not objs:
|
|
106
|
-
return objs
|
|
107
|
-
|
|
108
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
109
|
-
raise TypeError(
|
|
110
|
-
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Fire hooks before DB ops
|
|
114
|
-
if not bypass_hooks:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if not
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
""
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
#
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
returned_columns = base_qs._batched_insert(
|
|
442
|
-
|
|
443
|
-
fields,
|
|
444
|
-
batch_size=len(
|
|
445
|
-
)
|
|
446
|
-
for
|
|
447
|
-
for result, field in zip(results, opts.db_returning_fields):
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if hasattr(
|
|
483
|
-
|
|
484
|
-
if
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if hasattr(
|
|
516
|
-
|
|
517
|
-
if
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1
|
+
from django.db import models, transaction
|
|
2
|
+
from django.db.models import AutoField
|
|
3
|
+
|
|
4
|
+
from django_bulk_hooks import engine
|
|
5
|
+
from django_bulk_hooks.constants import (
|
|
6
|
+
AFTER_CREATE,
|
|
7
|
+
AFTER_DELETE,
|
|
8
|
+
AFTER_UPDATE,
|
|
9
|
+
BEFORE_CREATE,
|
|
10
|
+
BEFORE_DELETE,
|
|
11
|
+
BEFORE_UPDATE,
|
|
12
|
+
VALIDATE_CREATE,
|
|
13
|
+
VALIDATE_DELETE,
|
|
14
|
+
VALIDATE_UPDATE,
|
|
15
|
+
)
|
|
16
|
+
from django_bulk_hooks.context import HookContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HookQuerySet(models.QuerySet):
|
|
20
|
+
CHUNK_SIZE = 200
|
|
21
|
+
|
|
22
|
+
@transaction.atomic
|
|
23
|
+
def delete(self):
|
|
24
|
+
objs = list(self)
|
|
25
|
+
if not objs:
|
|
26
|
+
return 0
|
|
27
|
+
return self.model.objects.bulk_delete(objs)
|
|
28
|
+
|
|
29
|
+
@transaction.atomic
|
|
30
|
+
def update(self, **kwargs):
|
|
31
|
+
instances = list(self)
|
|
32
|
+
if not instances:
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
model_cls = self.model
|
|
36
|
+
pks = [obj.pk for obj in instances]
|
|
37
|
+
|
|
38
|
+
# Load originals for hook comparison and ensure they match the order of instances
|
|
39
|
+
original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
|
|
40
|
+
originals = [original_map.get(obj.pk) for obj in instances]
|
|
41
|
+
|
|
42
|
+
# Apply field updates to instances
|
|
43
|
+
for obj in instances:
|
|
44
|
+
for field, value in kwargs.items():
|
|
45
|
+
setattr(obj, field, value)
|
|
46
|
+
|
|
47
|
+
# Run BEFORE_UPDATE hooks
|
|
48
|
+
ctx = HookContext(model_cls)
|
|
49
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
50
|
+
|
|
51
|
+
# Use Django's built-in update logic directly
|
|
52
|
+
queryset = self.model.objects.filter(pk__in=pks)
|
|
53
|
+
update_count = queryset.update(**kwargs)
|
|
54
|
+
|
|
55
|
+
# Run AFTER_UPDATE hooks
|
|
56
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
57
|
+
|
|
58
|
+
return update_count
|
|
59
|
+
|
|
60
|
+
@transaction.atomic
|
|
61
|
+
def bulk_create(
|
|
62
|
+
self,
|
|
63
|
+
objs,
|
|
64
|
+
batch_size=None,
|
|
65
|
+
ignore_conflicts=False,
|
|
66
|
+
update_conflicts=False,
|
|
67
|
+
update_fields=None,
|
|
68
|
+
unique_fields=None,
|
|
69
|
+
bypass_hooks=False,
|
|
70
|
+
bypass_validation=False,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
74
|
+
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
75
|
+
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
76
|
+
"""
|
|
77
|
+
model_cls = self.model
|
|
78
|
+
|
|
79
|
+
# When you bulk insert you don't get the primary keys back (if it's an
|
|
80
|
+
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
81
|
+
# you can't insert into the child tables which references this. There
|
|
82
|
+
# are two workarounds:
|
|
83
|
+
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
84
|
+
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
85
|
+
# tables to get the primary keys back and then doing a single bulk
|
|
86
|
+
# insert into the childmost table.
|
|
87
|
+
# We currently set the primary keys on the objects when using
|
|
88
|
+
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
89
|
+
# Oracle as well, but the semantics for extracting the primary keys is
|
|
90
|
+
# trickier so it's not done yet.
|
|
91
|
+
if batch_size is not None and batch_size <= 0:
|
|
92
|
+
raise ValueError("Batch size must be a positive integer.")
|
|
93
|
+
|
|
94
|
+
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
95
|
+
# This follows Django's approach: check that the parents share the same concrete model
|
|
96
|
+
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
97
|
+
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
98
|
+
# identify that case as involving multiple tables.
|
|
99
|
+
is_mti = False
|
|
100
|
+
for parent in model_cls._meta.all_parents:
|
|
101
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
102
|
+
is_mti = True
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
if not objs:
|
|
106
|
+
return objs
|
|
107
|
+
|
|
108
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
109
|
+
raise TypeError(
|
|
110
|
+
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Fire hooks before DB ops
|
|
114
|
+
if not bypass_hooks:
|
|
115
|
+
print(f"DEBUG: Firing BEFORE_CREATE hooks for {model_cls}")
|
|
116
|
+
ctx = HookContext(model_cls)
|
|
117
|
+
if not bypass_validation:
|
|
118
|
+
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
119
|
+
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
120
|
+
else:
|
|
121
|
+
print(f"DEBUG: Skipping hooks due to bypass_hooks=True for {model_cls}")
|
|
122
|
+
|
|
123
|
+
# For MTI models, we need to handle them specially
|
|
124
|
+
if is_mti:
|
|
125
|
+
# Use our MTI-specific logic
|
|
126
|
+
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
127
|
+
mti_kwargs = {
|
|
128
|
+
'batch_size': batch_size,
|
|
129
|
+
'ignore_conflicts': ignore_conflicts,
|
|
130
|
+
'update_conflicts': update_conflicts,
|
|
131
|
+
'update_fields': update_fields,
|
|
132
|
+
'unique_fields': unique_fields,
|
|
133
|
+
}
|
|
134
|
+
# Remove custom hook kwargs if present in self.bulk_create signature
|
|
135
|
+
result = self._mti_bulk_create(
|
|
136
|
+
objs,
|
|
137
|
+
**{k: v for k, v in mti_kwargs.items() if k not in ['bypass_hooks', 'bypass_validation']}
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
# For single-table models, use Django's built-in bulk_create
|
|
141
|
+
# but we need to call it on the base manager to avoid recursion
|
|
142
|
+
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
143
|
+
|
|
144
|
+
# Use Django's original QuerySet to avoid recursive calls
|
|
145
|
+
from django.db.models import QuerySet
|
|
146
|
+
original_qs = QuerySet(model_cls, using=self.db)
|
|
147
|
+
result = original_qs.bulk_create(
|
|
148
|
+
objs,
|
|
149
|
+
batch_size=batch_size,
|
|
150
|
+
ignore_conflicts=ignore_conflicts,
|
|
151
|
+
update_conflicts=update_conflicts,
|
|
152
|
+
update_fields=update_fields,
|
|
153
|
+
unique_fields=unique_fields,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Fire AFTER_CREATE hooks
|
|
157
|
+
if not bypass_hooks:
|
|
158
|
+
print(f"DEBUG: Firing AFTER_CREATE hooks for {model_cls}")
|
|
159
|
+
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
160
|
+
else:
|
|
161
|
+
print(f"DEBUG: Skipping AFTER_CREATE hooks due to bypass_hooks=True for {model_cls}")
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
@transaction.atomic
|
|
166
|
+
def bulk_update(self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
167
|
+
"""
|
|
168
|
+
Bulk update objects in the database.
|
|
169
|
+
"""
|
|
170
|
+
import inspect
|
|
171
|
+
print(f"DEBUG: QuerySet.bulk_update called with:")
|
|
172
|
+
print(f" - self: {type(self)}")
|
|
173
|
+
print(f" - objs: {type(objs)}")
|
|
174
|
+
print(f" - fields: {fields}")
|
|
175
|
+
print(f" - bypass_hooks: {bypass_hooks}")
|
|
176
|
+
print(f" - bypass_validation: {bypass_validation}")
|
|
177
|
+
print(f" - kwargs: {kwargs}")
|
|
178
|
+
print(f"DEBUG: Method signature: {inspect.signature(self.bulk_update)}")
|
|
179
|
+
|
|
180
|
+
model_cls = self.model
|
|
181
|
+
print(f"DEBUG: Model class: {model_cls}")
|
|
182
|
+
print(f"DEBUG: bypass_hooks value: {bypass_hooks}")
|
|
183
|
+
|
|
184
|
+
if not objs:
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
188
|
+
raise TypeError(
|
|
189
|
+
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not bypass_hooks:
|
|
193
|
+
# Load originals for hook comparison and ensure they match the order of new instances
|
|
194
|
+
original_map = {
|
|
195
|
+
obj.pk: obj
|
|
196
|
+
for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
|
|
197
|
+
}
|
|
198
|
+
originals = [original_map.get(obj.pk) for obj in objs]
|
|
199
|
+
|
|
200
|
+
ctx = HookContext(model_cls)
|
|
201
|
+
|
|
202
|
+
# Run validation hooks first
|
|
203
|
+
if not bypass_validation:
|
|
204
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
205
|
+
|
|
206
|
+
# Then run business logic hooks
|
|
207
|
+
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
208
|
+
|
|
209
|
+
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
210
|
+
modified_fields = self._detect_modified_fields(objs, originals)
|
|
211
|
+
if modified_fields:
|
|
212
|
+
# Convert to set for efficient union operation
|
|
213
|
+
fields_set = set(fields)
|
|
214
|
+
fields_set.update(modified_fields)
|
|
215
|
+
fields = list(fields_set)
|
|
216
|
+
|
|
217
|
+
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
218
|
+
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
219
|
+
|
|
220
|
+
# Call the base implementation to avoid re-triggering this method
|
|
221
|
+
# Filter out custom parameters that Django's bulk_update doesn't accept
|
|
222
|
+
django_kwargs = {k: v for k, v in kwargs.items() if k not in ['bypass_hooks', 'bypass_validation']}
|
|
223
|
+
super().bulk_update(chunk, fields, **django_kwargs)
|
|
224
|
+
|
|
225
|
+
if not bypass_hooks:
|
|
226
|
+
print(f"DEBUG: Firing AFTER_UPDATE hooks for {model_cls}")
|
|
227
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
228
|
+
else:
|
|
229
|
+
print(f"DEBUG: Skipping AFTER_UPDATE hooks due to bypass_hooks=True for {model_cls}")
|
|
230
|
+
|
|
231
|
+
return objs
|
|
232
|
+
|
|
233
|
+
@transaction.atomic
|
|
234
|
+
def bulk_delete(
|
|
235
|
+
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
236
|
+
):
|
|
237
|
+
if not objs:
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
model_cls = self.model
|
|
241
|
+
|
|
242
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
243
|
+
raise TypeError(
|
|
244
|
+
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
ctx = HookContext(model_cls)
|
|
248
|
+
|
|
249
|
+
if not bypass_hooks:
|
|
250
|
+
# Run validation hooks first
|
|
251
|
+
if not bypass_validation:
|
|
252
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
253
|
+
|
|
254
|
+
# Then run business logic hooks
|
|
255
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
256
|
+
|
|
257
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
258
|
+
|
|
259
|
+
# Use base manager for the actual deletion to prevent recursion
|
|
260
|
+
# The hooks have already been fired above, so we don't need them again
|
|
261
|
+
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
262
|
+
|
|
263
|
+
if not bypass_hooks:
|
|
264
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
265
|
+
|
|
266
|
+
return objs
|
|
267
|
+
|
|
268
|
+
def _detect_modified_fields(self, new_instances, original_instances):
|
|
269
|
+
"""
|
|
270
|
+
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
271
|
+
new instances with their original values.
|
|
272
|
+
"""
|
|
273
|
+
if not original_instances:
|
|
274
|
+
return set()
|
|
275
|
+
|
|
276
|
+
modified_fields = set()
|
|
277
|
+
|
|
278
|
+
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
279
|
+
for new_instance, original in zip(new_instances, original_instances):
|
|
280
|
+
if new_instance.pk is None or original is None:
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Compare all fields to detect changes
|
|
284
|
+
for field in new_instance._meta.fields:
|
|
285
|
+
if field.name == "id":
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
new_value = getattr(new_instance, field.name)
|
|
289
|
+
original_value = getattr(original, field.name)
|
|
290
|
+
|
|
291
|
+
# Handle different field types appropriately
|
|
292
|
+
if field.is_relation:
|
|
293
|
+
# For foreign keys, compare the pk values
|
|
294
|
+
new_pk = new_value.pk if new_value else None
|
|
295
|
+
original_pk = original_value.pk if original_value else None
|
|
296
|
+
if new_pk != original_pk:
|
|
297
|
+
modified_fields.add(field.name)
|
|
298
|
+
else:
|
|
299
|
+
# For regular fields, use direct comparison
|
|
300
|
+
if new_value != original_value:
|
|
301
|
+
modified_fields.add(field.name)
|
|
302
|
+
|
|
303
|
+
return modified_fields
|
|
304
|
+
|
|
305
|
+
def _get_inheritance_chain(self):
|
|
306
|
+
"""
|
|
307
|
+
Get the complete inheritance chain from root parent to current model.
|
|
308
|
+
Returns list of model classes in order: [RootParent, Parent, Child]
|
|
309
|
+
"""
|
|
310
|
+
chain = []
|
|
311
|
+
current_model = self.model
|
|
312
|
+
while current_model:
|
|
313
|
+
if not current_model._meta.proxy:
|
|
314
|
+
chain.append(current_model)
|
|
315
|
+
parents = [
|
|
316
|
+
parent
|
|
317
|
+
for parent in current_model._meta.parents.keys()
|
|
318
|
+
if not parent._meta.proxy
|
|
319
|
+
]
|
|
320
|
+
current_model = parents[0] if parents else None
|
|
321
|
+
chain.reverse()
|
|
322
|
+
return chain
|
|
323
|
+
|
|
324
|
+
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
325
|
+
"""
|
|
326
|
+
Implements Django's suggested workaround #2 for MTI bulk_create:
|
|
327
|
+
O(n) normal inserts into parent tables to get primary keys back,
|
|
328
|
+
then single bulk insert into childmost table.
|
|
329
|
+
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
330
|
+
"""
|
|
331
|
+
# Remove custom hook kwargs before passing to Django internals
|
|
332
|
+
django_kwargs = {k: v for k, v in kwargs.items() if k not in ['bypass_hooks', 'bypass_validation']}
|
|
333
|
+
if inheritance_chain is None:
|
|
334
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
335
|
+
|
|
336
|
+
# Safety check to prevent infinite recursion
|
|
337
|
+
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
338
|
+
raise ValueError(
|
|
339
|
+
"Inheritance chain too deep - possible infinite recursion detected"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
343
|
+
created_objects = []
|
|
344
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
345
|
+
for i in range(0, len(objs), batch_size):
|
|
346
|
+
batch = objs[i : i + batch_size]
|
|
347
|
+
batch_result = self._process_mti_batch(
|
|
348
|
+
batch, inheritance_chain, **django_kwargs
|
|
349
|
+
)
|
|
350
|
+
created_objects.extend(batch_result)
|
|
351
|
+
return created_objects
|
|
352
|
+
|
|
353
|
+
def _process_mti_batch(self, batch, inheritance_chain, **kwargs):
|
|
354
|
+
"""
|
|
355
|
+
Process a single batch of objects through the inheritance chain.
|
|
356
|
+
Implements Django's suggested workaround #2: O(n) normal inserts into parent
|
|
357
|
+
tables to get primary keys back, then single bulk insert into childmost table.
|
|
358
|
+
"""
|
|
359
|
+
# For MTI, we need to save parent objects first to get PKs
|
|
360
|
+
# Then we can use Django's bulk_create for the child objects
|
|
361
|
+
parent_objects_map = {}
|
|
362
|
+
|
|
363
|
+
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
364
|
+
# Get bypass_hooks from kwargs
|
|
365
|
+
bypass_hooks = kwargs.get('bypass_hooks', False)
|
|
366
|
+
bypass_validation = kwargs.get('bypass_validation', False)
|
|
367
|
+
|
|
368
|
+
for obj in batch:
|
|
369
|
+
parent_instances = {}
|
|
370
|
+
current_parent = None
|
|
371
|
+
for model_class in inheritance_chain[:-1]:
|
|
372
|
+
parent_obj = self._create_parent_instance(
|
|
373
|
+
obj, model_class, current_parent
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Fire parent hooks if not bypassed
|
|
377
|
+
if not bypass_hooks:
|
|
378
|
+
ctx = HookContext(model_class)
|
|
379
|
+
if not bypass_validation:
|
|
380
|
+
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
381
|
+
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
382
|
+
|
|
383
|
+
# Use Django's base manager to create the object and get PKs back
|
|
384
|
+
# This bypasses hooks and the MTI exception
|
|
385
|
+
field_values = {
|
|
386
|
+
field.name: getattr(parent_obj, field.name)
|
|
387
|
+
for field in model_class._meta.local_fields
|
|
388
|
+
if hasattr(parent_obj, field.name) and getattr(parent_obj, field.name) is not None
|
|
389
|
+
}
|
|
390
|
+
created_obj = model_class._base_manager.using(self.db).create(**field_values)
|
|
391
|
+
|
|
392
|
+
# Update the parent_obj with the created object's PK
|
|
393
|
+
parent_obj.pk = created_obj.pk
|
|
394
|
+
parent_obj._state.adding = False
|
|
395
|
+
parent_obj._state.db = self.db
|
|
396
|
+
|
|
397
|
+
# Fire AFTER_CREATE hooks for parent
|
|
398
|
+
if not bypass_hooks:
|
|
399
|
+
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
400
|
+
|
|
401
|
+
parent_instances[model_class] = parent_obj
|
|
402
|
+
current_parent = parent_obj
|
|
403
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
404
|
+
|
|
405
|
+
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
406
|
+
child_model = inheritance_chain[-1]
|
|
407
|
+
all_child_objects = []
|
|
408
|
+
for obj in batch:
|
|
409
|
+
child_obj = self._create_child_instance(
|
|
410
|
+
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
411
|
+
)
|
|
412
|
+
all_child_objects.append(child_obj)
|
|
413
|
+
|
|
414
|
+
# Step 2.5: Use Django's internal bulk_create infrastructure
|
|
415
|
+
if all_child_objects:
|
|
416
|
+
# Get the base manager's queryset
|
|
417
|
+
base_qs = child_model._base_manager.using(self.db)
|
|
418
|
+
|
|
419
|
+
# Use Django's exact approach: call _prepare_for_bulk_create then partition
|
|
420
|
+
base_qs._prepare_for_bulk_create(all_child_objects)
|
|
421
|
+
|
|
422
|
+
# Implement our own partition since itertools.partition might not be available
|
|
423
|
+
objs_without_pk, objs_with_pk = [], []
|
|
424
|
+
for obj in all_child_objects:
|
|
425
|
+
if obj._is_pk_set():
|
|
426
|
+
objs_with_pk.append(obj)
|
|
427
|
+
else:
|
|
428
|
+
objs_without_pk.append(obj)
|
|
429
|
+
|
|
430
|
+
# Use Django's internal _batched_insert method
|
|
431
|
+
opts = child_model._meta
|
|
432
|
+
# For child models in MTI, we need to include the foreign key to the parent
|
|
433
|
+
# but exclude the primary key since it's inherited
|
|
434
|
+
|
|
435
|
+
# Include all local fields except generated ones
|
|
436
|
+
# We need to include the foreign key to the parent (business_ptr)
|
|
437
|
+
fields = [f for f in opts.local_fields if not f.generated]
|
|
438
|
+
|
|
439
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
440
|
+
if objs_with_pk:
|
|
441
|
+
returned_columns = base_qs._batched_insert(
|
|
442
|
+
objs_with_pk,
|
|
443
|
+
fields,
|
|
444
|
+
batch_size=len(objs_with_pk), # Use actual batch size
|
|
445
|
+
)
|
|
446
|
+
for obj_with_pk, results in zip(objs_with_pk, returned_columns):
|
|
447
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
448
|
+
if field != opts.pk:
|
|
449
|
+
setattr(obj_with_pk, field.attname, result)
|
|
450
|
+
for obj_with_pk in objs_with_pk:
|
|
451
|
+
obj_with_pk._state.adding = False
|
|
452
|
+
obj_with_pk._state.db = self.db
|
|
453
|
+
|
|
454
|
+
if objs_without_pk:
|
|
455
|
+
# For objects without PK, we still need to exclude primary key fields
|
|
456
|
+
fields = [f for f in fields if not isinstance(f, AutoField) and not f.primary_key]
|
|
457
|
+
returned_columns = base_qs._batched_insert(
|
|
458
|
+
objs_without_pk,
|
|
459
|
+
fields,
|
|
460
|
+
batch_size=len(objs_without_pk), # Use actual batch size
|
|
461
|
+
)
|
|
462
|
+
for obj_without_pk, results in zip(objs_without_pk, returned_columns):
|
|
463
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
464
|
+
setattr(obj_without_pk, field.attname, result)
|
|
465
|
+
obj_without_pk._state.adding = False
|
|
466
|
+
obj_without_pk._state.db = self.db
|
|
467
|
+
|
|
468
|
+
# Step 3: Update original objects with generated PKs and state
|
|
469
|
+
pk_field_name = child_model._meta.pk.name
|
|
470
|
+
for orig_obj, child_obj in zip(batch, all_child_objects):
|
|
471
|
+
child_pk = getattr(child_obj, pk_field_name)
|
|
472
|
+
setattr(orig_obj, pk_field_name, child_pk)
|
|
473
|
+
orig_obj._state.adding = False
|
|
474
|
+
orig_obj._state.db = self.db
|
|
475
|
+
|
|
476
|
+
return batch
|
|
477
|
+
|
|
478
|
+
def _create_parent_instance(self, source_obj, parent_model, current_parent):
|
|
479
|
+
parent_obj = parent_model()
|
|
480
|
+
for field in parent_model._meta.local_fields:
|
|
481
|
+
# Only copy if the field exists on the source and is not None
|
|
482
|
+
if hasattr(source_obj, field.name):
|
|
483
|
+
value = getattr(source_obj, field.name, None)
|
|
484
|
+
if value is not None:
|
|
485
|
+
setattr(parent_obj, field.name, value)
|
|
486
|
+
if current_parent is not None:
|
|
487
|
+
for field in parent_model._meta.local_fields:
|
|
488
|
+
if (
|
|
489
|
+
hasattr(field, "remote_field")
|
|
490
|
+
and field.remote_field
|
|
491
|
+
and field.remote_field.model == current_parent.__class__
|
|
492
|
+
):
|
|
493
|
+
setattr(parent_obj, field.name, current_parent)
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
# Handle auto_now_add and auto_now fields like Django does
|
|
497
|
+
for field in parent_model._meta.local_fields:
|
|
498
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
499
|
+
# Ensure auto_now_add fields are properly set
|
|
500
|
+
if getattr(parent_obj, field.name) is None:
|
|
501
|
+
field.pre_save(parent_obj, add=True)
|
|
502
|
+
# Explicitly set the value to ensure it's not None
|
|
503
|
+
setattr(parent_obj, field.name, field.value_from_object(parent_obj))
|
|
504
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
505
|
+
field.pre_save(parent_obj, add=True)
|
|
506
|
+
|
|
507
|
+
return parent_obj
|
|
508
|
+
|
|
509
|
+
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
510
|
+
child_obj = child_model()
|
|
511
|
+
# Only copy fields that exist in the child model's local fields
|
|
512
|
+
for field in child_model._meta.local_fields:
|
|
513
|
+
if isinstance(field, AutoField):
|
|
514
|
+
continue
|
|
515
|
+
if hasattr(source_obj, field.name):
|
|
516
|
+
value = getattr(source_obj, field.name, None)
|
|
517
|
+
if value is not None:
|
|
518
|
+
setattr(child_obj, field.name, value)
|
|
519
|
+
|
|
520
|
+
# Set parent links for MTI
|
|
521
|
+
for parent_model, parent_instance in parent_instances.items():
|
|
522
|
+
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
523
|
+
if parent_link:
|
|
524
|
+
# Set both the foreign key value (the ID) and the object reference
|
|
525
|
+
# This follows Django's pattern in _set_pk_val
|
|
526
|
+
setattr(child_obj, parent_link.attname, parent_instance.pk) # Set the foreign key value
|
|
527
|
+
setattr(child_obj, parent_link.name, parent_instance) # Set the object reference
|
|
528
|
+
|
|
529
|
+
# Handle auto_now_add and auto_now fields like Django does
|
|
530
|
+
for field in child_model._meta.local_fields:
|
|
531
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
532
|
+
# Ensure auto_now_add fields are properly set
|
|
533
|
+
if getattr(child_obj, field.name) is None:
|
|
534
|
+
field.pre_save(child_obj, add=True)
|
|
535
|
+
# Explicitly set the value to ensure it's not None
|
|
536
|
+
setattr(child_obj, field.name, field.value_from_object(child_obj))
|
|
537
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
538
|
+
field.pre_save(child_obj, add=True)
|
|
539
|
+
|
|
540
|
+
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.163"
|
|
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
|