django-bulk-hooks 0.1.249__tar.gz → 0.1.250__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.

Files changed (17) hide show
  1. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/queryset.py +345 -105
  3. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/README.md +0 -0
  6. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.249 → django_bulk_hooks-0.1.250}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.249
3
+ Version: 0.1.250
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  Home-page: https://github.com/AugendLimited/django-bulk-hooks
6
6
  License: MIT
@@ -1,9 +1,11 @@
1
1
  import logging
2
2
 
3
3
  from django.db import models, transaction
4
- from django.db.models import AutoField, Case, Field, Subquery, Value, When
4
+ from django.db.models import AutoField, Case, Field, Value, When
5
5
 
6
6
  from django_bulk_hooks import engine
7
+
8
+ logger = logging.getLogger(__name__)
7
9
  from django_bulk_hooks.constants import (
8
10
  AFTER_CREATE,
9
11
  AFTER_DELETE,
@@ -18,12 +20,9 @@ from django_bulk_hooks.constants import (
18
20
  from django_bulk_hooks.context import (
19
21
  HookContext,
20
22
  get_bulk_update_value_map,
21
- get_bypass_hooks,
22
23
  set_bulk_update_value_map,
23
24
  )
24
25
 
25
- logger = logging.getLogger(__name__)
26
-
27
26
 
28
27
  class HookQuerySetMixin:
29
28
  """
@@ -56,81 +55,372 @@ class HookQuerySetMixin:
56
55
 
57
56
  @transaction.atomic
58
57
  def update(self, **kwargs):
59
- """Simplified update method that handles hooks cleanly."""
60
58
  logger.debug(f"Entering update method with {len(kwargs)} kwargs")
61
59
  instances = list(self)
62
60
  if not instances:
63
61
  return 0
64
62
 
65
63
  model_cls = self.model
66
- pks = [obj.pk for obj in instances if obj.pk is not None]
67
-
68
- # For test compatibility - if no PKs, create mock PKs
69
- if not pks and instances:
70
- for i, instance in enumerate(instances):
71
- if instance.pk is None:
72
- instance.pk = i + 1
73
- pks = [obj.pk for obj in instances]
64
+ pks = [obj.pk for obj in instances]
74
65
 
75
- # Load originals for hook comparison
66
+ # Load originals for hook comparison and ensure they match the order of instances
67
+ # Use the base manager to avoid recursion
76
68
  original_map = {
77
69
  obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
78
70
  }
79
71
  originals = [original_map.get(obj.pk) for obj in instances]
80
72
 
81
- # Check for Subquery updates
82
- from django.db.models import Subquery, Value
73
+ # Check if any of the update values are Subquery objects
74
+ try:
75
+ from django.db.models import Subquery
83
76
 
84
- has_subquery = any(isinstance(v, Subquery) for v in kwargs.values())
85
- logger.debug(f"Subquery detection result: {has_subquery}")
77
+ logger.debug(f"Successfully imported Subquery from django.db.models")
78
+ except ImportError as e:
79
+ logger.error(f"Failed to import Subquery: {e}")
80
+ raise
86
81
 
87
- # Skip hooks if bypassed
88
- from django_bulk_hooks.context import (
89
- get_bulk_update_value_map,
90
- get_bypass_hooks,
82
+ logger.debug(f"Checking for Subquery objects in {len(kwargs)} kwargs")
83
+
84
+ subquery_detected = []
85
+ for key, value in kwargs.items():
86
+ is_subquery = isinstance(value, Subquery)
87
+ logger.debug(
88
+ f"Key '{key}': type={type(value).__name__}, is_subquery={is_subquery}"
89
+ )
90
+ if is_subquery:
91
+ subquery_detected.append(key)
92
+
93
+ has_subquery = len(subquery_detected) > 0
94
+ logger.debug(
95
+ f"Subquery detection result: {has_subquery}, detected keys: {subquery_detected}"
91
96
  )
92
97
 
93
- if get_bypass_hooks():
94
- logger.debug("update: hooks explicitly bypassed")
95
- return super().update(**kwargs)
98
+ # Debug logging for Subquery detection
99
+ logger.debug(f"Update kwargs: {list(kwargs.keys())}")
100
+ logger.debug(
101
+ f"Update kwargs types: {[(k, type(v).__name__) for k, v in kwargs.items()]}"
102
+ )
96
103
 
97
- ctx = HookContext(model_cls, bypass_hooks=False)
98
- logger.debug("update: running hooks with Salesforce-style behavior")
104
+ if has_subquery:
105
+ logger.debug(
106
+ f"Detected Subquery in update: {[k for k, v in kwargs.items() if isinstance(v, Subquery)]}"
107
+ )
108
+ else:
109
+ # Check if we missed any Subquery objects
110
+ for k, v in kwargs.items():
111
+ if hasattr(v, "query") and hasattr(v, "resolve_expression"):
112
+ logger.warning(
113
+ f"Potential Subquery-like object detected but not recognized: {k}={type(v).__name__}"
114
+ )
115
+ logger.warning(
116
+ f"Object attributes: query={hasattr(v, 'query')}, resolve_expression={hasattr(v, 'resolve_expression')}"
117
+ )
118
+ logger.warning(
119
+ f"Object dir: {[attr for attr in dir(v) if not attr.startswith('_')][:10]}"
120
+ )
121
+
122
+ # Apply field updates to instances
123
+ # If a per-object value map exists (from bulk_update), prefer it over kwargs
124
+ # IMPORTANT: Do not assign Django expression objects (e.g., Subquery/Case/F)
125
+ # to in-memory instances before running BEFORE_UPDATE hooks. Hooks must not
126
+ # receive unresolved expression objects.
127
+ per_object_values = get_bulk_update_value_map()
99
128
 
129
+ # For Subquery updates, skip all in-memory field assignments to prevent
130
+ # expression objects from reaching hooks
100
131
  if has_subquery:
101
- # For Subquery updates: database first, then hooks
102
- logger.debug("Using two-stage update for Subquery")
132
+ logger.debug(
133
+ "Skipping in-memory field assignments due to Subquery detection"
134
+ )
135
+ else:
136
+ for obj in instances:
137
+ if per_object_values and obj.pk in per_object_values:
138
+ for field, value in per_object_values[obj.pk].items():
139
+ setattr(obj, field, value)
140
+ else:
141
+ for field, value in kwargs.items():
142
+ # Skip assigning expression-like objects (they will be handled at DB level)
143
+ is_expression_like = hasattr(value, "resolve_expression")
144
+ if is_expression_like:
145
+ # Special-case Value() which can be unwrapped safely
146
+ if isinstance(value, Value):
147
+ try:
148
+ setattr(obj, field, value.value)
149
+ except Exception:
150
+ # If Value cannot be unwrapped for any reason, skip assignment
151
+ continue
152
+ else:
153
+ # Do not assign unresolved expressions to in-memory objects
154
+ logger.debug(
155
+ f"Skipping assignment of expression {type(value).__name__} to field {field}"
156
+ )
157
+ continue
158
+ else:
159
+ setattr(obj, field, value)
160
+
161
+ # Salesforce-style trigger behavior: Always run hooks, rely on Django's stack overflow protection
162
+ from django_bulk_hooks.context import get_bypass_hooks
163
+
164
+ current_bypass_hooks = get_bypass_hooks()
165
+
166
+ # Only skip hooks if explicitly bypassed (not for recursion prevention)
167
+ if current_bypass_hooks:
168
+ logger.debug("update: hooks explicitly bypassed")
169
+ ctx = HookContext(model_cls, bypass_hooks=True)
170
+ else:
171
+ # Always run hooks - Django will handle stack overflow protection
172
+ logger.debug("update: running hooks with Salesforce-style behavior")
173
+ ctx = HookContext(model_cls, bypass_hooks=False)
174
+
175
+ # Run validation hooks first
176
+ engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
177
+
178
+ # For Subquery updates, skip BEFORE_UPDATE hooks here - they'll run after refresh
179
+ if not has_subquery:
180
+ # Then run BEFORE_UPDATE hooks for non-Subquery updates
181
+ engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
182
+
183
+ # Persist any additional field mutations made by BEFORE_UPDATE hooks.
184
+ # Build CASE statements per modified field not already present in kwargs.
185
+ # Note: For Subquery updates, this will be empty since hooks haven't run yet
186
+ # For Subquery updates, hook modifications are handled later via bulk_update
187
+ if not has_subquery:
188
+ modified_fields = self._detect_modified_fields(instances, originals)
189
+ extra_fields = [f for f in modified_fields if f not in kwargs]
190
+ else:
191
+ extra_fields = [] # Skip for Subquery updates
192
+
193
+ if extra_fields:
194
+ case_statements = {}
195
+ for field_name in extra_fields:
196
+ try:
197
+ field_obj = model_cls._meta.get_field(field_name)
198
+ except Exception:
199
+ # Skip unknown fields
200
+ continue
201
+
202
+ when_statements = []
203
+ for obj in instances:
204
+ obj_pk = getattr(obj, "pk", None)
205
+ if obj_pk is None:
206
+ continue
207
+
208
+ # Determine value and output field
209
+ if getattr(field_obj, "is_relation", False):
210
+ # For FK fields, store the raw id and target field output type
211
+ value = getattr(obj, field_obj.attname, None)
212
+ output_field = field_obj.target_field
213
+ target_name = (
214
+ field_obj.attname
215
+ ) # use column name (e.g., fk_id)
216
+ else:
217
+ value = getattr(obj, field_name)
218
+ output_field = field_obj
219
+ target_name = field_name
220
+
221
+ # Special handling for Subquery and other expression values in CASE statements
222
+ if isinstance(value, Subquery):
223
+ logger.debug(
224
+ f"Creating When statement with Subquery for {field_name}"
225
+ )
226
+ # Ensure the Subquery has proper output_field
227
+ if (
228
+ not hasattr(value, "output_field")
229
+ or value.output_field is None
230
+ ):
231
+ value.output_field = output_field
232
+ logger.debug(
233
+ f"Set output_field for Subquery in When statement to {output_field}"
234
+ )
235
+ when_statements.append(When(pk=obj_pk, then=value))
236
+ elif hasattr(value, "resolve_expression"):
237
+ # Handle other expression objects (Case, F, etc.)
238
+ logger.debug(
239
+ f"Creating When statement with expression for {field_name}: {type(value).__name__}"
240
+ )
241
+ when_statements.append(When(pk=obj_pk, then=value))
242
+ else:
243
+ when_statements.append(
244
+ When(
245
+ pk=obj_pk,
246
+ then=Value(value, output_field=output_field),
247
+ )
248
+ )
249
+
250
+ if when_statements:
251
+ case_statements[target_name] = Case(
252
+ *when_statements, output_field=output_field
253
+ )
103
254
 
104
- # Stage 1: Execute the database update first
105
- logger.debug("Stage 1: Executing Subquery update")
106
- update_count = super().update(**kwargs)
107
- logger.debug(f"Subquery update completed, affected {update_count} records")
255
+ # Merge extra CASE updates into kwargs for DB update
256
+ if case_statements:
257
+ logger.debug(
258
+ f"Adding case statements to kwargs: {list(case_statements.keys())}"
259
+ )
260
+ for field_name, case_stmt in case_statements.items():
261
+ logger.debug(
262
+ f"Case statement for {field_name}: {type(case_stmt).__name__}"
263
+ )
264
+ # Check if the case statement contains Subquery objects
265
+ if hasattr(case_stmt, "get_source_expressions"):
266
+ source_exprs = case_stmt.get_source_expressions()
267
+ for expr in source_exprs:
268
+ if isinstance(expr, Subquery):
269
+ logger.debug(
270
+ f"Case statement for {field_name} contains Subquery"
271
+ )
272
+ elif hasattr(expr, "get_source_expressions"):
273
+ # Check nested expressions (like Value objects)
274
+ nested_exprs = expr.get_source_expressions()
275
+ for nested_expr in nested_exprs:
276
+ if isinstance(nested_expr, Subquery):
277
+ logger.debug(
278
+ f"Case statement for {field_name} contains nested Subquery"
279
+ )
280
+
281
+ kwargs = {**kwargs, **case_statements}
282
+
283
+ # Use Django's built-in update logic directly
284
+ # Call the base QuerySet implementation to avoid recursion
285
+
286
+ # Additional safety check: ensure Subquery objects are properly handled
287
+ # This prevents the "cannot adapt type 'Subquery'" error
288
+ safe_kwargs = {}
289
+ logger.debug(f"Processing {len(kwargs)} kwargs for safety check")
290
+
291
+ for key, value in kwargs.items():
292
+ logger.debug(
293
+ f"Processing key '{key}' with value type {type(value).__name__}"
294
+ )
108
295
 
109
- # Stage 2: Refresh instances with computed values
110
- logger.debug("Stage 2: Refreshing instances with Subquery results")
111
- refreshed_map = {
296
+ if isinstance(value, Subquery):
297
+ logger.debug(f"Found Subquery for field {key}")
298
+ # Ensure Subquery has proper output_field
299
+ if not hasattr(value, "output_field") or value.output_field is None:
300
+ logger.warning(
301
+ f"Subquery for field {key} missing output_field, attempting to infer"
302
+ )
303
+ # Try to infer from the model field
304
+ try:
305
+ field = model_cls._meta.get_field(key)
306
+ logger.debug(f"Inferred field type: {type(field).__name__}")
307
+ value = value.resolve_expression(None, None)
308
+ value.output_field = field
309
+ logger.debug(f"Set output_field to {field}")
310
+ except Exception as e:
311
+ logger.error(
312
+ f"Failed to infer output_field for Subquery on {key}: {e}"
313
+ )
314
+ raise
315
+ else:
316
+ logger.debug(
317
+ f"Subquery for field {key} already has output_field: {value.output_field}"
318
+ )
319
+ safe_kwargs[key] = value
320
+ elif hasattr(value, "get_source_expressions") and hasattr(
321
+ value, "resolve_expression"
322
+ ):
323
+ # Handle Case statements and other complex expressions
324
+ logger.debug(
325
+ f"Found complex expression for field {key}: {type(value).__name__}"
326
+ )
327
+
328
+ # Check if this expression contains any Subquery objects
329
+ source_expressions = value.get_source_expressions()
330
+ has_nested_subquery = False
331
+
332
+ for expr in source_expressions:
333
+ if isinstance(expr, Subquery):
334
+ has_nested_subquery = True
335
+ logger.debug(f"Found nested Subquery in {type(value).__name__}")
336
+ # Ensure the nested Subquery has proper output_field
337
+ if (
338
+ not hasattr(expr, "output_field")
339
+ or expr.output_field is None
340
+ ):
341
+ try:
342
+ field = model_cls._meta.get_field(key)
343
+ expr.output_field = field
344
+ logger.debug(
345
+ f"Set output_field for nested Subquery to {field}"
346
+ )
347
+ except Exception as e:
348
+ logger.error(
349
+ f"Failed to set output_field for nested Subquery: {e}"
350
+ )
351
+ raise
352
+
353
+ if has_nested_subquery:
354
+ logger.debug(
355
+ f"Expression contains Subquery, ensuring proper output_field"
356
+ )
357
+ # Try to resolve the expression to ensure it's properly formatted
358
+ try:
359
+ resolved_value = value.resolve_expression(None, None)
360
+ safe_kwargs[key] = resolved_value
361
+ logger.debug(f"Successfully resolved expression for {key}")
362
+ except Exception as e:
363
+ logger.error(f"Failed to resolve expression for {key}: {e}")
364
+ raise
365
+ else:
366
+ safe_kwargs[key] = value
367
+ else:
368
+ logger.debug(
369
+ f"Non-Subquery value for field {key}: {type(value).__name__}"
370
+ )
371
+ safe_kwargs[key] = value
372
+
373
+ logger.debug(f"Safe kwargs keys: {list(safe_kwargs.keys())}")
374
+ logger.debug(
375
+ f"Safe kwargs types: {[(k, type(v).__name__) for k, v in safe_kwargs.items()]}"
376
+ )
377
+
378
+ logger.debug(f"Calling super().update() with {len(safe_kwargs)} kwargs")
379
+ try:
380
+ update_count = super().update(**safe_kwargs)
381
+ logger.debug(f"Super update successful, count: {update_count}")
382
+ except Exception as e:
383
+ logger.error(f"Super update failed: {e}")
384
+ logger.error(f"Exception type: {type(e).__name__}")
385
+ logger.error(f"Safe kwargs that caused failure: {safe_kwargs}")
386
+ raise
387
+
388
+ # If we used Subquery objects, refresh the instances to get computed values
389
+ # and run BEFORE_UPDATE hooks so HasChanged conditions work correctly
390
+ if has_subquery and instances and not current_bypass_hooks:
391
+ logger.debug(
392
+ "Refreshing instances with Subquery computed values before running hooks"
393
+ )
394
+ # Simple refresh of model fields without fetching related objects
395
+ # Subquery updates only affect the model's own fields, not relationships
396
+ refreshed_instances = {
112
397
  obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
113
398
  }
399
+
400
+ # Bulk update all instances in memory and save pre-hook state
114
401
  pre_hook_state = {}
115
402
  for instance in instances:
116
- if instance.pk in refreshed_map:
117
- refreshed = refreshed_map[instance.pk]
118
- # Save pre-hook state for comparison
403
+ if instance.pk in refreshed_instances:
404
+ refreshed_instance = refreshed_instances[instance.pk]
405
+ # Save current state before modifying for hook comparison
119
406
  pre_hook_values = {}
120
407
  for field in model_cls._meta.fields:
121
408
  if field.name != "id":
122
- field_value = getattr(refreshed, field.name)
123
- pre_hook_values[field.name] = field_value
124
- setattr(instance, field.name, field_value)
409
+ pre_hook_values[field.name] = getattr(
410
+ refreshed_instance, field.name
411
+ )
412
+ setattr(
413
+ instance,
414
+ field.name,
415
+ getattr(refreshed_instance, field.name),
416
+ )
125
417
  pre_hook_state[instance.pk] = pre_hook_values
126
418
 
127
- # Stage 3: Run hooks with refreshed data
128
- logger.debug("Stage 3: Running hooks with refreshed instances")
129
- engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
419
+ # Now run BEFORE_UPDATE hooks with refreshed instances so conditions work
420
+ logger.debug("Running BEFORE_UPDATE hooks after Subquery refresh")
130
421
  engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
131
422
 
132
- # Stage 4: Persist hook modifications
133
- logger.debug("Stage 4: Detecting hook modifications for bulk_update")
423
+ # Check if hooks modified any fields and persist them with bulk_update
134
424
  hook_modified_fields = set()
135
425
  for instance in instances:
136
426
  if instance.pk in pre_hook_state:
@@ -140,65 +430,22 @@ class HookQuerySetMixin:
140
430
  if current_value != pre_hook_value:
141
431
  hook_modified_fields.add(field_name)
142
432
 
433
+ hook_modified_fields = list(hook_modified_fields)
143
434
  if hook_modified_fields:
144
- hook_modified_fields = list(hook_modified_fields)
145
435
  logger.debug(
146
436
  f"Running bulk_update for hook-modified fields: {hook_modified_fields}"
147
437
  )
438
+ # Use bulk_update to persist hook modifications, bypassing hooks to avoid recursion
148
439
  model_cls.objects.bulk_update(
149
440
  instances, hook_modified_fields, bypass_hooks=True
150
441
  )
151
442
 
443
+ # Salesforce-style: Always run AFTER_UPDATE hooks unless explicitly bypassed
444
+ if not current_bypass_hooks:
445
+ logger.debug("update: running AFTER_UPDATE")
446
+ engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
152
447
  else:
153
- # For regular updates: hooks first, then database
154
- logger.debug("Using single-stage update for non-Subquery")
155
-
156
- # Apply field updates to instances
157
- per_object_values = get_bulk_update_value_map()
158
- for instance in instances:
159
- if per_object_values and instance.pk in per_object_values:
160
- for field, value in per_object_values[instance.pk].items():
161
- setattr(instance, field, value)
162
- else:
163
- for field, value in kwargs.items():
164
- # Skip assigning expression-like objects (they will be handled at DB level)
165
- if hasattr(value, "resolve_expression"):
166
- # Special-case Value() which can be unwrapped safely
167
- if isinstance(value, Value):
168
- try:
169
- setattr(instance, field, value.value)
170
- except Exception:
171
- continue
172
- else:
173
- logger.debug(
174
- f"Skipping assignment of expression {type(value).__name__} to field {field}"
175
- )
176
- continue
177
- else:
178
- setattr(instance, field, value)
179
-
180
- # Run hooks
181
- engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
182
- engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
183
-
184
- # Execute database update
185
- update_count = super().update(**kwargs)
186
- logger.debug(f"Super update successful, count: {update_count}")
187
-
188
- # Detect and persist additional hook modifications
189
- hook_modified_fields = self._detect_modified_fields(instances, originals)
190
- extra_fields = [f for f in hook_modified_fields if f not in kwargs]
191
- if extra_fields:
192
- logger.debug(
193
- f"Running bulk_update for hook-modified fields: {extra_fields}"
194
- )
195
- model_cls.objects.bulk_update(
196
- instances, extra_fields, bypass_hooks=True
197
- )
198
-
199
- # Always run AFTER_UPDATE hooks
200
- logger.debug("update: running AFTER_UPDATE")
201
- engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
448
+ logger.debug("update: AFTER_UPDATE explicitly bypassed")
202
449
 
203
450
  return update_count
204
451
 
@@ -372,14 +619,7 @@ class HookQuerySetMixin:
372
619
  field_values = {}
373
620
  for field_name in fields:
374
621
  # Capture raw values assigned on the object (not expressions)
375
- value = getattr(obj, field_name)
376
- # Skip expression objects that should not be passed to hooks
377
- if hasattr(value, "resolve_expression"):
378
- logger.debug(
379
- f"Skipping expression {type(value).__name__} for field {field_name} in bulk_update value map"
380
- )
381
- continue
382
- field_values[field_name] = value
622
+ field_values[field_name] = getattr(obj, field_name)
383
623
  if field_values:
384
624
  value_map[obj.pk] = field_values
385
625
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.249"
3
+ version = "0.1.250"
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"