django-bulk-hooks 0.2.16__py3-none-any.whl → 0.2.19__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/__init__.py +20 -24
- django_bulk_hooks/changeset.py +1 -1
- django_bulk_hooks/conditions.py +8 -12
- django_bulk_hooks/decorators.py +16 -18
- django_bulk_hooks/dispatcher.py +9 -10
- django_bulk_hooks/factory.py +36 -38
- django_bulk_hooks/handler.py +5 -6
- django_bulk_hooks/helpers.py +4 -3
- django_bulk_hooks/models.py +12 -13
- django_bulk_hooks/operations/__init__.py +5 -5
- django_bulk_hooks/operations/analyzer.py +14 -14
- django_bulk_hooks/operations/bulk_executor.py +79 -71
- django_bulk_hooks/operations/coordinator.py +61 -61
- django_bulk_hooks/operations/mti_handler.py +67 -65
- django_bulk_hooks/operations/mti_plans.py +17 -16
- django_bulk_hooks/operations/record_classifier.py +22 -21
- django_bulk_hooks/queryset.py +5 -3
- django_bulk_hooks/registry.py +40 -45
- {django_bulk_hooks-0.2.16.dist-info → django_bulk_hooks-0.2.19.dist-info}/METADATA +1 -1
- django_bulk_hooks-0.2.19.dist-info/RECORD +26 -0
- django_bulk_hooks-0.2.16.dist-info/RECORD +0 -26
- {django_bulk_hooks-0.2.16.dist-info → django_bulk_hooks-0.2.19.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.16.dist-info → django_bulk_hooks-0.2.19.dist-info}/WHEEL +0 -0
|
@@ -5,14 +5,14 @@ This module contains all services for bulk operations following
|
|
|
5
5
|
a clean, service-based architecture.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from django_bulk_hooks.operations.coordinator import BulkOperationCoordinator
|
|
9
8
|
from django_bulk_hooks.operations.analyzer import ModelAnalyzer
|
|
10
9
|
from django_bulk_hooks.operations.bulk_executor import BulkExecutor
|
|
10
|
+
from django_bulk_hooks.operations.coordinator import BulkOperationCoordinator
|
|
11
11
|
from django_bulk_hooks.operations.mti_handler import MTIHandler
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
"BulkExecutor",
|
|
15
|
+
"BulkOperationCoordinator",
|
|
16
|
+
"MTIHandler",
|
|
17
|
+
"ModelAnalyzer",
|
|
18
18
|
]
|
|
@@ -84,7 +84,7 @@ class ModelAnalyzer:
|
|
|
84
84
|
if invalid_types:
|
|
85
85
|
raise TypeError(
|
|
86
86
|
f"{operation} expected instances of {self.model_cls.__name__}, "
|
|
87
|
-
f"but got {invalid_types}"
|
|
87
|
+
f"but got {invalid_types}",
|
|
88
88
|
)
|
|
89
89
|
|
|
90
90
|
def _check_has_pks(self, objs, operation="operation"):
|
|
@@ -94,7 +94,7 @@ class ModelAnalyzer:
|
|
|
94
94
|
if missing_pks:
|
|
95
95
|
raise ValueError(
|
|
96
96
|
f"{operation} cannot operate on unsaved {self.model_cls.__name__} instances. "
|
|
97
|
-
f"{len(missing_pks)} object(s) have no primary key."
|
|
97
|
+
f"{len(missing_pks)} object(s) have no primary key.",
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
# ========== Data Fetching Methods ==========
|
|
@@ -130,7 +130,7 @@ class ModelAnalyzer:
|
|
|
130
130
|
auto_now_fields = []
|
|
131
131
|
for field in self.model_cls._meta.fields:
|
|
132
132
|
if getattr(field, "auto_now", False) or getattr(
|
|
133
|
-
field, "auto_now_add", False
|
|
133
|
+
field, "auto_now_add", False,
|
|
134
134
|
):
|
|
135
135
|
auto_now_fields.append(field.name)
|
|
136
136
|
return auto_now_fields
|
|
@@ -224,28 +224,28 @@ class ModelAnalyzer:
|
|
|
224
224
|
"""
|
|
225
225
|
from django.db.models import Expression
|
|
226
226
|
from django.db.models.expressions import Combinable
|
|
227
|
-
|
|
227
|
+
|
|
228
228
|
# Simple value - return as-is
|
|
229
229
|
if not isinstance(expression, (Expression, Combinable)):
|
|
230
230
|
return expression
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
# For complex expressions, evaluate them in database context
|
|
233
233
|
# Use annotate() which Django properly handles for all expression types
|
|
234
234
|
try:
|
|
235
235
|
# Create a queryset for just this instance
|
|
236
236
|
instance_qs = self.model_cls.objects.filter(pk=instance.pk)
|
|
237
|
-
|
|
237
|
+
|
|
238
238
|
# Use annotate with the expression and let Django resolve it
|
|
239
239
|
resolved_value = instance_qs.annotate(
|
|
240
|
-
_resolved_value=expression
|
|
241
|
-
).values_list(
|
|
242
|
-
|
|
240
|
+
_resolved_value=expression,
|
|
241
|
+
).values_list("_resolved_value", flat=True).first()
|
|
242
|
+
|
|
243
243
|
return resolved_value
|
|
244
244
|
except Exception as e:
|
|
245
245
|
# If expression resolution fails, log and return original
|
|
246
246
|
logger.warning(
|
|
247
247
|
f"Failed to resolve expression for field '{field_name}' "
|
|
248
|
-
f"on {self.model_cls.__name__}: {e}. Using original value."
|
|
248
|
+
f"on {self.model_cls.__name__}: {e}. Using original value.",
|
|
249
249
|
)
|
|
250
250
|
return expression
|
|
251
251
|
|
|
@@ -266,12 +266,12 @@ class ModelAnalyzer:
|
|
|
266
266
|
"""
|
|
267
267
|
if not instances or not update_kwargs:
|
|
268
268
|
return []
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
fields_updated = list(update_kwargs.keys())
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
for field_name, value in update_kwargs.items():
|
|
273
273
|
for instance in instances:
|
|
274
274
|
resolved_value = self.resolve_expression(field_name, value, instance)
|
|
275
275
|
setattr(instance, field_name, resolved_value)
|
|
276
|
-
|
|
277
|
-
return fields_updated
|
|
276
|
+
|
|
277
|
+
return fields_updated
|
|
@@ -5,6 +5,7 @@ This service coordinates bulk database operations with validation and MTI handli
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
|
|
8
9
|
from django.db import transaction
|
|
9
10
|
from django.db.models import AutoField
|
|
10
11
|
|
|
@@ -71,7 +72,7 @@ class BulkExecutor:
|
|
|
71
72
|
# Check if this is an MTI model and route accordingly
|
|
72
73
|
if self.mti_handler.is_mti_model():
|
|
73
74
|
logger.info(f"Detected MTI model {self.model_cls.__name__}, using MTI bulk create")
|
|
74
|
-
|
|
75
|
+
|
|
75
76
|
# Classify records using the classifier service
|
|
76
77
|
existing_record_ids = set()
|
|
77
78
|
existing_pks_map = {}
|
|
@@ -79,7 +80,7 @@ class BulkExecutor:
|
|
|
79
80
|
existing_record_ids, existing_pks_map = (
|
|
80
81
|
self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
81
82
|
)
|
|
82
|
-
|
|
83
|
+
|
|
83
84
|
# Build execution plan with classification results
|
|
84
85
|
plan = self.mti_handler.build_create_plan(
|
|
85
86
|
objs,
|
|
@@ -182,51 +183,50 @@ class BulkExecutor:
|
|
|
182
183
|
Returns:
|
|
183
184
|
List of created/updated objects with PKs assigned
|
|
184
185
|
"""
|
|
185
|
-
from django.db import transaction
|
|
186
186
|
from django.db.models import QuerySet as BaseQuerySet
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
if not plan:
|
|
189
189
|
return []
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
with transaction.atomic(using=self.queryset.db, savepoint=False):
|
|
192
192
|
# Step 1: Create/Update all parent objects level by level
|
|
193
193
|
parent_instances_map = {} # Maps original obj id() -> {model: parent_instance}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
for parent_level in plan.parent_levels:
|
|
196
196
|
# Separate new and existing parent objects
|
|
197
197
|
new_parents = []
|
|
198
198
|
existing_parents = []
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
for parent_obj in parent_level.objects:
|
|
201
201
|
orig_obj_id = parent_level.original_object_map[id(parent_obj)]
|
|
202
202
|
if orig_obj_id in plan.existing_record_ids:
|
|
203
203
|
existing_parents.append(parent_obj)
|
|
204
204
|
else:
|
|
205
205
|
new_parents.append(parent_obj)
|
|
206
|
-
|
|
206
|
+
|
|
207
207
|
# Bulk create new parents
|
|
208
208
|
if new_parents:
|
|
209
209
|
bulk_kwargs = {"batch_size": len(new_parents)}
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
if parent_level.update_conflicts:
|
|
212
212
|
bulk_kwargs["update_conflicts"] = True
|
|
213
213
|
bulk_kwargs["unique_fields"] = parent_level.unique_fields
|
|
214
214
|
bulk_kwargs["update_fields"] = parent_level.update_fields
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
# Use base QuerySet to avoid recursion
|
|
217
217
|
base_qs = BaseQuerySet(model=parent_level.model_class, using=self.queryset.db)
|
|
218
218
|
created_parents = base_qs.bulk_create(new_parents, **bulk_kwargs)
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
# Copy generated fields back to parent objects
|
|
221
221
|
for created_parent, parent_obj in zip(created_parents, new_parents):
|
|
222
222
|
for field in parent_level.model_class._meta.local_fields:
|
|
223
223
|
created_value = getattr(created_parent, field.name, None)
|
|
224
224
|
if created_value is not None:
|
|
225
225
|
setattr(parent_obj, field.name, created_value)
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
parent_obj._state.adding = False
|
|
228
228
|
parent_obj._state.db = self.queryset.db
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
# Update existing parents
|
|
231
231
|
if existing_parents and parent_level.update_fields:
|
|
232
232
|
# Filter update fields to only those that exist in this parent model
|
|
@@ -237,53 +237,53 @@ class BulkExecutor:
|
|
|
237
237
|
field for field in parent_level.update_fields
|
|
238
238
|
if field in parent_model_fields
|
|
239
239
|
]
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
if filtered_update_fields:
|
|
242
242
|
base_qs = BaseQuerySet(model=parent_level.model_class, using=self.queryset.db)
|
|
243
243
|
base_qs.bulk_update(existing_parents, filtered_update_fields)
|
|
244
|
-
|
|
244
|
+
|
|
245
245
|
# Mark as not adding
|
|
246
246
|
for parent_obj in existing_parents:
|
|
247
247
|
parent_obj._state.adding = False
|
|
248
248
|
parent_obj._state.db = self.queryset.db
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
# Map parents back to original objects
|
|
251
251
|
for parent_obj in parent_level.objects:
|
|
252
252
|
orig_obj_id = parent_level.original_object_map[id(parent_obj)]
|
|
253
253
|
if orig_obj_id not in parent_instances_map:
|
|
254
254
|
parent_instances_map[orig_obj_id] = {}
|
|
255
255
|
parent_instances_map[orig_obj_id][parent_level.model_class] = parent_obj
|
|
256
|
-
|
|
256
|
+
|
|
257
257
|
# Step 2: Add parent links to child objects and separate new/existing
|
|
258
258
|
new_child_objects = []
|
|
259
259
|
existing_child_objects = []
|
|
260
|
-
|
|
260
|
+
|
|
261
261
|
for child_obj, orig_obj in zip(plan.child_objects, plan.original_objects):
|
|
262
262
|
parent_instances = parent_instances_map.get(id(orig_obj), {})
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
# Set parent links
|
|
265
265
|
for parent_model, parent_instance in parent_instances.items():
|
|
266
266
|
parent_link = plan.child_model._meta.get_ancestor_link(parent_model)
|
|
267
267
|
if parent_link:
|
|
268
268
|
setattr(child_obj, parent_link.attname, parent_instance.pk)
|
|
269
269
|
setattr(child_obj, parent_link.name, parent_instance)
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
# Classify as new or existing
|
|
272
272
|
if id(orig_obj) in plan.existing_record_ids:
|
|
273
273
|
# For existing records, set the PK on child object
|
|
274
|
-
pk_value = getattr(orig_obj,
|
|
274
|
+
pk_value = getattr(orig_obj, "pk", None)
|
|
275
275
|
if pk_value:
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
child_obj.pk = pk_value
|
|
277
|
+
child_obj.id = pk_value
|
|
278
278
|
existing_child_objects.append(child_obj)
|
|
279
279
|
else:
|
|
280
280
|
new_child_objects.append(child_obj)
|
|
281
|
-
|
|
281
|
+
|
|
282
282
|
# Step 3: Bulk create new child objects using _batched_insert (to bypass MTI check)
|
|
283
283
|
if new_child_objects:
|
|
284
284
|
base_qs = BaseQuerySet(model=plan.child_model, using=self.queryset.db)
|
|
285
285
|
base_qs._prepare_for_bulk_create(new_child_objects)
|
|
286
|
-
|
|
286
|
+
|
|
287
287
|
# Partition objects by PK status
|
|
288
288
|
objs_without_pk, objs_with_pk = [], []
|
|
289
289
|
for obj in new_child_objects:
|
|
@@ -291,11 +291,11 @@ class BulkExecutor:
|
|
|
291
291
|
objs_with_pk.append(obj)
|
|
292
292
|
else:
|
|
293
293
|
objs_without_pk.append(obj)
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
# Get fields for insert
|
|
296
296
|
opts = plan.child_model._meta
|
|
297
297
|
fields = [f for f in opts.local_fields if not f.generated]
|
|
298
|
-
|
|
298
|
+
|
|
299
299
|
# Execute bulk insert
|
|
300
300
|
if objs_with_pk:
|
|
301
301
|
returned_columns = base_qs._batched_insert(
|
|
@@ -315,7 +315,7 @@ class BulkExecutor:
|
|
|
315
315
|
for obj in objs_with_pk:
|
|
316
316
|
obj._state.adding = False
|
|
317
317
|
obj._state.db = self.queryset.db
|
|
318
|
-
|
|
318
|
+
|
|
319
319
|
if objs_without_pk:
|
|
320
320
|
filtered_fields = [
|
|
321
321
|
f for f in fields
|
|
@@ -337,7 +337,7 @@ class BulkExecutor:
|
|
|
337
337
|
for obj in objs_without_pk:
|
|
338
338
|
obj._state.adding = False
|
|
339
339
|
obj._state.db = self.queryset.db
|
|
340
|
-
|
|
340
|
+
|
|
341
341
|
# Step 3.5: Update existing child objects
|
|
342
342
|
if existing_child_objects and plan.update_fields:
|
|
343
343
|
# Filter update fields to only those that exist in the child model
|
|
@@ -348,30 +348,30 @@ class BulkExecutor:
|
|
|
348
348
|
field for field in plan.update_fields
|
|
349
349
|
if field in child_model_fields
|
|
350
350
|
]
|
|
351
|
-
|
|
351
|
+
|
|
352
352
|
if filtered_child_update_fields:
|
|
353
353
|
base_qs = BaseQuerySet(model=plan.child_model, using=self.queryset.db)
|
|
354
354
|
base_qs.bulk_update(existing_child_objects, filtered_child_update_fields)
|
|
355
|
-
|
|
355
|
+
|
|
356
356
|
# Mark as not adding
|
|
357
357
|
for child_obj in existing_child_objects:
|
|
358
358
|
child_obj._state.adding = False
|
|
359
359
|
child_obj._state.db = self.queryset.db
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
# Combine all children for final processing
|
|
362
362
|
created_children = new_child_objects + existing_child_objects
|
|
363
|
-
|
|
363
|
+
|
|
364
364
|
# Step 4: Copy PKs and auto-generated fields back to original objects
|
|
365
365
|
pk_field_name = plan.child_model._meta.pk.name
|
|
366
|
-
|
|
366
|
+
|
|
367
367
|
for orig_obj, child_obj in zip(plan.original_objects, created_children):
|
|
368
368
|
# Copy PK
|
|
369
369
|
child_pk = getattr(child_obj, pk_field_name)
|
|
370
370
|
setattr(orig_obj, pk_field_name, child_pk)
|
|
371
|
-
|
|
371
|
+
|
|
372
372
|
# Copy auto-generated fields from all levels
|
|
373
373
|
parent_instances = parent_instances_map.get(id(orig_obj), {})
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
for model_class in plan.inheritance_chain:
|
|
376
376
|
# Get source object for this level
|
|
377
377
|
if model_class in parent_instances:
|
|
@@ -380,30 +380,30 @@ class BulkExecutor:
|
|
|
380
380
|
source_obj = child_obj
|
|
381
381
|
else:
|
|
382
382
|
continue
|
|
383
|
-
|
|
383
|
+
|
|
384
384
|
# Copy auto-generated field values
|
|
385
385
|
for field in model_class._meta.local_fields:
|
|
386
386
|
if field.name == pk_field_name:
|
|
387
387
|
continue
|
|
388
|
-
|
|
388
|
+
|
|
389
389
|
# Skip parent link fields
|
|
390
|
-
if hasattr(field,
|
|
390
|
+
if hasattr(field, "remote_field") and field.remote_field:
|
|
391
391
|
parent_link = plan.child_model._meta.get_ancestor_link(model_class)
|
|
392
392
|
if parent_link and field.name == parent_link.name:
|
|
393
393
|
continue
|
|
394
|
-
|
|
394
|
+
|
|
395
395
|
# Copy auto_now_add, auto_now, and db_returning fields
|
|
396
|
-
if (getattr(field,
|
|
397
|
-
getattr(field,
|
|
398
|
-
getattr(field,
|
|
396
|
+
if (getattr(field, "auto_now_add", False) or
|
|
397
|
+
getattr(field, "auto_now", False) or
|
|
398
|
+
getattr(field, "db_returning", False)):
|
|
399
399
|
source_value = getattr(source_obj, field.name, None)
|
|
400
400
|
if source_value is not None:
|
|
401
401
|
setattr(orig_obj, field.name, source_value)
|
|
402
|
-
|
|
402
|
+
|
|
403
403
|
# Update object state
|
|
404
404
|
orig_obj._state.adding = False
|
|
405
405
|
orig_obj._state.db = self.queryset.db
|
|
406
|
-
|
|
406
|
+
|
|
407
407
|
return plan.original_objects
|
|
408
408
|
|
|
409
409
|
def _execute_mti_update_plan(self, plan):
|
|
@@ -418,86 +418,94 @@ class BulkExecutor:
|
|
|
418
418
|
Returns:
|
|
419
419
|
Number of objects updated
|
|
420
420
|
"""
|
|
421
|
-
from django.db import
|
|
422
|
-
from django.db.models import
|
|
423
|
-
|
|
421
|
+
from django.db.models import Case
|
|
422
|
+
from django.db.models import QuerySet as BaseQuerySet
|
|
423
|
+
from django.db.models import Value
|
|
424
|
+
from django.db.models import When
|
|
425
|
+
|
|
424
426
|
if not plan:
|
|
425
427
|
return 0
|
|
426
|
-
|
|
428
|
+
|
|
427
429
|
total_updated = 0
|
|
428
|
-
|
|
430
|
+
|
|
429
431
|
# Get PKs for filtering
|
|
430
432
|
root_pks = [
|
|
431
|
-
getattr(obj, "pk", None) or getattr(obj, "id", None)
|
|
432
|
-
for obj in plan.objects
|
|
433
|
+
getattr(obj, "pk", None) or getattr(obj, "id", None)
|
|
434
|
+
for obj in plan.objects
|
|
433
435
|
if getattr(obj, "pk", None) or getattr(obj, "id", None)
|
|
434
436
|
]
|
|
435
|
-
|
|
437
|
+
|
|
436
438
|
if not root_pks:
|
|
437
439
|
return 0
|
|
438
|
-
|
|
440
|
+
|
|
439
441
|
with transaction.atomic(using=self.queryset.db, savepoint=False):
|
|
440
442
|
# Update each table in the chain
|
|
441
443
|
for field_group in plan.field_groups:
|
|
442
444
|
if not field_group.fields:
|
|
443
445
|
continue
|
|
444
|
-
|
|
446
|
+
|
|
445
447
|
base_qs = BaseQuerySet(model=field_group.model_class, using=self.queryset.db)
|
|
446
|
-
|
|
448
|
+
|
|
447
449
|
# Check if records exist
|
|
448
450
|
existing_count = base_qs.filter(**{f"{field_group.filter_field}__in": root_pks}).count()
|
|
449
451
|
if existing_count == 0:
|
|
450
452
|
continue
|
|
451
|
-
|
|
453
|
+
|
|
452
454
|
# Build CASE statements for bulk update
|
|
453
455
|
case_statements = {}
|
|
454
456
|
for field_name in field_group.fields:
|
|
455
457
|
field = field_group.model_class._meta.get_field(field_name)
|
|
456
|
-
|
|
458
|
+
|
|
457
459
|
# Use column name for FK fields
|
|
458
|
-
if getattr(field,
|
|
460
|
+
if getattr(field, "is_relation", False) and hasattr(field, "attname"):
|
|
459
461
|
db_field_name = field.attname
|
|
460
462
|
target_field = field.target_field
|
|
461
463
|
else:
|
|
462
464
|
db_field_name = field_name
|
|
463
465
|
target_field = field
|
|
464
|
-
|
|
466
|
+
|
|
465
467
|
when_statements = []
|
|
466
468
|
for pk, obj in zip(root_pks, plan.objects):
|
|
467
469
|
obj_pk = getattr(obj, "pk", None) or getattr(obj, "id", None)
|
|
468
470
|
if obj_pk is None:
|
|
469
471
|
continue
|
|
470
|
-
|
|
472
|
+
|
|
471
473
|
value = getattr(obj, db_field_name)
|
|
472
|
-
|
|
474
|
+
|
|
473
475
|
# For FK fields, ensure we get the actual ID value, not the related object
|
|
474
|
-
if getattr(field,
|
|
476
|
+
if getattr(field, "is_relation", False) and hasattr(field, "attname"):
|
|
475
477
|
# If value is a model instance, get its pk
|
|
476
|
-
if value is not None and hasattr(value,
|
|
478
|
+
if value is not None and hasattr(value, "pk"):
|
|
477
479
|
value = value.pk
|
|
478
|
-
|
|
480
|
+
# If value is a string representation of an ID, convert to int
|
|
481
|
+
elif value is not None and isinstance(value, str) and value.isdigit():
|
|
482
|
+
value = int(value)
|
|
483
|
+
# If value is None or empty string, ensure it's None
|
|
484
|
+
elif value == "":
|
|
485
|
+
value = None
|
|
486
|
+
|
|
479
487
|
when_statements.append(
|
|
480
488
|
When(
|
|
481
489
|
**{field_group.filter_field: pk},
|
|
482
490
|
then=Value(value, output_field=target_field),
|
|
483
|
-
)
|
|
491
|
+
),
|
|
484
492
|
)
|
|
485
|
-
|
|
493
|
+
|
|
486
494
|
if when_statements:
|
|
487
495
|
case_statements[db_field_name] = Case(
|
|
488
|
-
*when_statements, output_field=target_field
|
|
496
|
+
*when_statements, output_field=target_field,
|
|
489
497
|
)
|
|
490
|
-
|
|
498
|
+
|
|
491
499
|
# Execute bulk update
|
|
492
500
|
if case_statements:
|
|
493
501
|
try:
|
|
494
502
|
updated_count = base_qs.filter(
|
|
495
|
-
**{f"{field_group.filter_field}__in": root_pks}
|
|
503
|
+
**{f"{field_group.filter_field}__in": root_pks},
|
|
496
504
|
).update(**case_statements)
|
|
497
505
|
total_updated += updated_count
|
|
498
506
|
except Exception as e:
|
|
499
507
|
logger.error(f"MTI bulk update failed for {field_group.model_class.__name__}: {e}")
|
|
500
|
-
|
|
508
|
+
|
|
501
509
|
return total_updated
|
|
502
510
|
|
|
503
511
|
def delete_queryset(self):
|