django-bulk-hooks 0.1.236__tar.gz → 0.1.238__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.236 → django_bulk_hooks-0.1.238}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/queryset.py +64 -13
  3. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/README.md +0 -0
  6. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.236 → django_bulk_hooks-0.1.238}/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.236
3
+ Version: 0.1.238
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,4 +1,5 @@
1
1
  import logging
2
+
2
3
  from django.db import models, transaction
3
4
  from django.db.models import AutoField, Case, Field, Value, When
4
5
 
@@ -16,8 +17,8 @@ from django_bulk_hooks.constants import (
16
17
  VALIDATE_DELETE,
17
18
  VALIDATE_UPDATE,
18
19
  )
19
- from django_bulk_hooks.context import HookContext
20
20
  from django_bulk_hooks.context import (
21
+ HookContext,
21
22
  get_bulk_update_value_map,
22
23
  set_bulk_update_value_map,
23
24
  )
@@ -87,8 +88,9 @@ class HookQuerySetMixin:
87
88
 
88
89
  # Check if we're in a bulk operation context to prevent double hook execution
89
90
  from django_bulk_hooks.context import get_bypass_hooks
91
+
90
92
  current_bypass_hooks = get_bypass_hooks()
91
-
93
+
92
94
  # If we're in a bulk operation context, skip hooks to prevent double execution
93
95
  if current_bypass_hooks:
94
96
  logger.debug("update: skipping hooks (bulk context)")
@@ -101,6 +103,53 @@ class HookQuerySetMixin:
101
103
  # Then run BEFORE_UPDATE hooks
102
104
  engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
103
105
 
106
+ # Persist any additional field mutations made by BEFORE_UPDATE hooks.
107
+ # Build CASE statements per modified field not already present in kwargs.
108
+ modified_fields = self._detect_modified_fields(instances, originals)
109
+ extra_fields = [f for f in modified_fields if f not in kwargs]
110
+ if extra_fields:
111
+ case_statements = {}
112
+ for field_name in extra_fields:
113
+ try:
114
+ field_obj = model_cls._meta.get_field(field_name)
115
+ except Exception:
116
+ # Skip unknown fields
117
+ continue
118
+
119
+ when_statements = []
120
+ for obj in instances:
121
+ obj_pk = getattr(obj, "pk", None)
122
+ if obj_pk is None:
123
+ continue
124
+
125
+ # Determine value and output field
126
+ if getattr(field_obj, "is_relation", False):
127
+ # For FK fields, store the raw id and target field output type
128
+ value = getattr(obj, field_obj.attname, None)
129
+ output_field = field_obj.target_field
130
+ target_name = (
131
+ field_obj.attname
132
+ ) # use column name (e.g., fk_id)
133
+ else:
134
+ value = getattr(obj, field_name)
135
+ output_field = field_obj
136
+ target_name = field_name
137
+
138
+ when_statements.append(
139
+ When(
140
+ pk=obj_pk, then=Value(value, output_field=output_field)
141
+ )
142
+ )
143
+
144
+ if when_statements:
145
+ case_statements[target_name] = Case(
146
+ *when_statements, output_field=output_field
147
+ )
148
+
149
+ # Merge extra CASE updates into kwargs for DB update
150
+ if case_statements:
151
+ kwargs = {**kwargs, **case_statements}
152
+
104
153
  # Use Django's built-in update logic directly
105
154
  # Call the base QuerySet implementation to avoid recursion
106
155
  update_count = super().update(**kwargs)
@@ -190,12 +239,12 @@ class HookQuerySetMixin:
190
239
 
191
240
  # Fire hooks before DB ops
192
241
  if not bypass_hooks:
193
- ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
242
+ ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
194
243
  if not bypass_validation:
195
244
  engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
196
245
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
197
246
  else:
198
- ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
247
+ ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
199
248
  logger.debug("bulk_create bypassed hooks")
200
249
 
201
250
  # For MTI models, we need to handle them specially
@@ -251,7 +300,9 @@ class HookQuerySetMixin:
251
300
  f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
252
301
  )
253
302
 
254
- logger.debug(f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}")
303
+ logger.debug(
304
+ f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
305
+ )
255
306
 
256
307
  # Check for MTI
257
308
  is_mti = False
@@ -267,7 +318,9 @@ class HookQuerySetMixin:
267
318
  else:
268
319
  logger.debug("bulk_update: hooks bypassed")
269
320
  ctx = HookContext(model_cls, bypass_hooks=True)
270
- originals = [None] * len(objs) # Ensure originals is defined for after_update call
321
+ originals = [None] * len(
322
+ objs
323
+ ) # Ensure originals is defined for after_update call
271
324
 
272
325
  # Handle auto_now fields like Django's update_or_create does
273
326
  fields_set = set(fields)
@@ -345,18 +398,16 @@ class HookQuerySetMixin:
345
398
  if field.name == "id":
346
399
  continue
347
400
 
348
- new_value = getattr(new_instance, field.name)
349
- original_value = getattr(original, field.name)
350
-
351
401
  # Handle different field types appropriately
352
402
  if field.is_relation:
353
- # For foreign keys, compare the pk values
354
- new_pk = new_value.pk if new_value else None
355
- original_pk = original_value.pk if original_value else None
403
+ # Compare by raw id values to catch cases where only <fk>_id was set
404
+ new_pk = getattr(new_instance, field.attname, None)
405
+ original_pk = getattr(original, field.attname, None)
356
406
  if new_pk != original_pk:
357
407
  modified_fields.add(field.name)
358
408
  else:
359
- # For regular fields, use direct comparison
409
+ new_value = getattr(new_instance, field.name)
410
+ original_value = getattr(original, field.name)
360
411
  if new_value != original_value:
361
412
  modified_fields.add(field.name)
362
413
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.236"
3
+ version = "0.1.238"
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"