django-bulk-hooks 0.2.62__tar.gz → 0.2.63__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 (27) hide show
  1. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/dispatcher.py +95 -1
  3. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/LICENSE +0 -0
  5. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/README.md +0 -0
  6. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/changeset.py +0 -0
  8. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/conditions.py +0 -0
  9. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/constants.py +0 -0
  10. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/context.py +0 -0
  11. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/decorators.py +0 -0
  12. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/factory.py +0 -0
  14. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/handler.py +0 -0
  15. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/helpers.py +0 -0
  16. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/manager.py +0 -0
  17. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/models.py +0 -0
  18. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/__init__.py +0 -0
  19. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/analyzer.py +0 -0
  20. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/bulk_executor.py +0 -0
  21. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/coordinator.py +0 -0
  22. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/field_utils.py +0 -0
  23. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/mti_handler.py +0 -0
  24. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/mti_plans.py +0 -0
  25. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/operations/record_classifier.py +0 -0
  26. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/queryset.py +0 -0
  27. {django_bulk_hooks-0.2.62 → django_bulk_hooks-0.2.63}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.62
3
+ Version: 0.2.63
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -122,7 +122,13 @@ class HookDispatcher:
122
122
  condition: Optional condition to filter records
123
123
  changeset: ChangeSet with all record changes
124
124
  """
125
- # Filter records based on condition
125
+ # NEW: Preload relationships needed for condition evaluation
126
+ if condition:
127
+ condition_relationships = self._extract_condition_relationships(condition, changeset.model_cls)
128
+ if condition_relationships:
129
+ self._preload_condition_relationships(changeset, condition_relationships)
130
+
131
+ # Filter records based on condition (now safe - relationships are preloaded)
126
132
  if condition:
127
133
  filtered_changes = [change for change in changeset.changes if condition.check(change.new_record, change.old_record)]
128
134
 
@@ -204,6 +210,94 @@ class HookDispatcher:
204
210
  )
205
211
  raise
206
212
 
213
+ def _extract_condition_relationships(self, condition, model_cls):
214
+ """
215
+ Extract relationship paths that a condition might access.
216
+
217
+ Args:
218
+ condition: HookCondition instance
219
+ model_cls: The model class
220
+
221
+ Returns:
222
+ set: Set of relationship field names to preload
223
+ """
224
+ relationships = set()
225
+
226
+ # Guard against Mock objects and non-condition objects
227
+ if not hasattr(condition, 'check') or hasattr(condition, '_mock_name'):
228
+ return relationships
229
+
230
+ # Handle different condition types
231
+ if hasattr(condition, 'field'):
232
+ # Extract relationships from field path (e.g., "status__value" -> "status")
233
+ field_path = condition.field
234
+ if isinstance(field_path, str):
235
+ if '__' in field_path:
236
+ # Take the first part before __ (the relationship to preload)
237
+ rel_field = field_path.split('__')[0]
238
+ relationships.add(rel_field)
239
+ elif self._is_relationship_field(model_cls, field_path):
240
+ relationships.add(field_path)
241
+
242
+ # Handle composite conditions (AndCondition, OrCondition)
243
+ if hasattr(condition, 'cond1') and hasattr(condition, 'cond2'):
244
+ relationships.update(self._extract_condition_relationships(condition.cond1, model_cls))
245
+ relationships.update(self._extract_condition_relationships(condition.cond2, model_cls))
246
+
247
+ # Handle NotCondition
248
+ if hasattr(condition, 'cond'):
249
+ relationships.update(self._extract_condition_relationships(condition.cond, model_cls))
250
+
251
+ return relationships
252
+
253
+ def _is_relationship_field(self, model_cls, field_name):
254
+ """Check if a field is a relationship field."""
255
+ try:
256
+ field = model_cls._meta.get_field(field_name)
257
+ return field.is_relation and not field.many_to_many
258
+ except:
259
+ return False
260
+
261
+ def _preload_condition_relationships(self, changeset, relationships):
262
+ """
263
+ Preload relationships needed for condition evaluation.
264
+
265
+ Args:
266
+ changeset: ChangeSet with records
267
+ relationships: Set of relationship field names to preload
268
+ """
269
+ if not relationships or not changeset.new_records:
270
+ return
271
+
272
+ # Use Django's select_related to preload relationships
273
+ relationship_list = list(relationships)
274
+
275
+ # Preload for new_records
276
+ if changeset.new_records:
277
+ # Use select_related on the queryset
278
+ ids = [obj.pk for obj in changeset.new_records if obj.pk is not None]
279
+ if ids:
280
+ preloaded = changeset.model_cls.objects.filter(pk__in=ids).select_related(*relationship_list).in_bulk()
281
+ # Update the objects in changeset with preloaded relationships
282
+ for obj in changeset.new_records:
283
+ if obj.pk and obj.pk in preloaded:
284
+ preloaded_obj = preloaded[obj.pk]
285
+ for rel in relationship_list:
286
+ if hasattr(preloaded_obj, rel):
287
+ setattr(obj, rel, getattr(preloaded_obj, rel))
288
+
289
+ # Also handle unsaved objects by preloading their FK targets
290
+ for obj in changeset.new_records:
291
+ if obj.pk is None: # Unsaved object
292
+ for rel in relationship_list:
293
+ if hasattr(obj, f'{rel}_id'):
294
+ rel_id = getattr(obj, f'{rel}_id')
295
+ if rel_id:
296
+ # Load the related object
297
+ rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field, 'model')
298
+ rel_obj = rel_model.objects.get(pk=rel_id)
299
+ setattr(obj, rel, rel_obj)
300
+
207
301
 
208
302
  # Global dispatcher instance
209
303
  _dispatcher: HookDispatcher | None = None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.62"
3
+ version = "0.2.63"
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"