django-bulk-hooks 0.1.118__tar.gz → 0.1.119__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 (18) hide show
  1. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/PKG-INFO +1 -1
  2. django_bulk_hooks-0.1.119/django_bulk_hooks/manager.py +201 -0
  3. django_bulk_hooks-0.1.118/django_bulk_hooks/manager.py → django_bulk_hooks-0.1.119/django_bulk_hooks/queryset.py +139 -208
  4. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/pyproject.toml +1 -1
  5. django_bulk_hooks-0.1.118/django_bulk_hooks/queryset.py +0 -44
  6. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/LICENSE +0 -0
  7. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/README.md +0 -0
  8. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/__init__.py +0 -0
  9. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/conditions.py +0 -0
  10. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/constants.py +0 -0
  11. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/context.py +0 -0
  12. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/decorators.py +0 -0
  13. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/engine.py +0 -0
  14. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/enums.py +0 -0
  15. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/handler.py +0 -0
  16. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/models.py +0 -0
  17. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/priority.py +0 -0
  18. {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/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.1.118
3
+ Version: 0.1.119
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
@@ -0,0 +1,201 @@
1
+ from django.db import models, transaction
2
+
3
+ from django_bulk_hooks import engine
4
+ from django_bulk_hooks.constants import (
5
+ AFTER_CREATE,
6
+ AFTER_DELETE,
7
+ AFTER_UPDATE,
8
+ BEFORE_CREATE,
9
+ BEFORE_DELETE,
10
+ BEFORE_UPDATE,
11
+ VALIDATE_CREATE,
12
+ VALIDATE_DELETE,
13
+ VALIDATE_UPDATE,
14
+ )
15
+ from django_bulk_hooks.context import HookContext
16
+ from django_bulk_hooks.queryset import HookQuerySet
17
+
18
+
19
+ class BulkHookManager(models.Manager):
20
+ CHUNK_SIZE = 200
21
+
22
+ def get_queryset(self):
23
+ return HookQuerySet(self.model, using=self._db)
24
+
25
+ @transaction.atomic
26
+ def bulk_update(
27
+ self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
28
+ ):
29
+ if not objs:
30
+ return []
31
+
32
+ model_cls = self.model
33
+
34
+ if any(not isinstance(obj, model_cls) for obj in objs):
35
+ raise TypeError(
36
+ f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
37
+ )
38
+
39
+ if not bypass_hooks:
40
+ # Load originals for hook comparison and ensure they match the order of new instances
41
+ original_map = {
42
+ obj.pk: obj
43
+ for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
44
+ }
45
+ originals = [original_map.get(obj.pk) for obj in objs]
46
+
47
+ ctx = HookContext(model_cls)
48
+
49
+ # Run validation hooks first
50
+ if not bypass_validation:
51
+ engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
52
+
53
+ # Then run business logic hooks
54
+ engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
55
+
56
+ # Automatically detect fields that were modified during BEFORE_UPDATE hooks
57
+ modified_fields = self._detect_modified_fields(objs, originals)
58
+ if modified_fields:
59
+ # Convert to set for efficient union operation
60
+ fields_set = set(fields)
61
+ fields_set.update(modified_fields)
62
+ fields = list(fields_set)
63
+
64
+ for i in range(0, len(objs), self.CHUNK_SIZE):
65
+ chunk = objs[i : i + self.CHUNK_SIZE]
66
+ # Call the base implementation to avoid re-triggering this method
67
+ super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
68
+
69
+ if not bypass_hooks:
70
+ engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
71
+
72
+ return objs
73
+
74
+ @transaction.atomic
75
+ def bulk_create(
76
+ self,
77
+ objs,
78
+ batch_size=None,
79
+ ignore_conflicts=False,
80
+ update_conflicts=False,
81
+ update_fields=None,
82
+ unique_fields=None,
83
+ bypass_hooks=False,
84
+ bypass_validation=False,
85
+ ):
86
+ """
87
+ Delegate to QuerySet's bulk_create implementation.
88
+ This follows Django's pattern where Manager methods call QuerySet methods.
89
+ """
90
+ return self.get_queryset().bulk_create(
91
+ objs,
92
+ batch_size=batch_size,
93
+ ignore_conflicts=ignore_conflicts,
94
+ update_conflicts=update_conflicts,
95
+ update_fields=update_fields,
96
+ unique_fields=unique_fields,
97
+ bypass_hooks=bypass_hooks,
98
+ bypass_validation=bypass_validation,
99
+ )
100
+
101
+ @transaction.atomic
102
+ def bulk_delete(
103
+ self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
104
+ ):
105
+ if not objs:
106
+ return []
107
+
108
+ model_cls = self.model
109
+
110
+ if any(not isinstance(obj, model_cls) for obj in objs):
111
+ raise TypeError(
112
+ f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
113
+ )
114
+
115
+ ctx = HookContext(model_cls)
116
+
117
+ if not bypass_hooks:
118
+ # Run validation hooks first
119
+ if not bypass_validation:
120
+ engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
121
+
122
+ # Then run business logic hooks
123
+ engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
124
+
125
+ pks = [obj.pk for obj in objs if obj.pk is not None]
126
+
127
+ # Use base manager for the actual deletion to prevent recursion
128
+ # The hooks have already been fired above, so we don't need them again
129
+ model_cls._base_manager.filter(pk__in=pks).delete()
130
+
131
+ if not bypass_hooks:
132
+ engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
133
+
134
+ return objs
135
+
136
+ @transaction.atomic
137
+ def update(self, **kwargs):
138
+ objs = list(self.all())
139
+ if not objs:
140
+ return 0
141
+ for key, value in kwargs.items():
142
+ for obj in objs:
143
+ setattr(obj, key, value)
144
+ self.bulk_update(objs, fields=list(kwargs.keys()))
145
+ return len(objs)
146
+
147
+ @transaction.atomic
148
+ def delete(self):
149
+ objs = list(self.all())
150
+ if not objs:
151
+ return 0
152
+ self.bulk_delete(objs)
153
+ return len(objs)
154
+
155
+ @transaction.atomic
156
+ def save(self, obj):
157
+ if obj.pk:
158
+ self.bulk_update(
159
+ [obj],
160
+ fields=[field.name for field in obj._meta.fields if field.name != "id"],
161
+ )
162
+ else:
163
+ self.bulk_create([obj])
164
+ return obj
165
+
166
+ def _detect_modified_fields(self, new_instances, original_instances):
167
+ """
168
+ Detect fields that were modified during BEFORE_UPDATE hooks by comparing
169
+ new instances with their original values.
170
+ """
171
+ if not original_instances:
172
+ return set()
173
+
174
+ modified_fields = set()
175
+
176
+ # Since original_instances is now ordered to match new_instances, we can zip them directly
177
+ for new_instance, original in zip(new_instances, original_instances):
178
+ if new_instance.pk is None or original is None:
179
+ continue
180
+
181
+ # Compare all fields to detect changes
182
+ for field in new_instance._meta.fields:
183
+ if field.name == "id":
184
+ continue
185
+
186
+ new_value = getattr(new_instance, field.name)
187
+ original_value = getattr(original, field.name)
188
+
189
+ # Handle different field types appropriately
190
+ if field.is_relation:
191
+ # For foreign keys, compare the pk values
192
+ new_pk = new_value.pk if new_value else None
193
+ original_pk = original_value.pk if original_value else None
194
+ if new_pk != original_pk:
195
+ modified_fields.add(field.name)
196
+ else:
197
+ # For regular fields, use direct comparison
198
+ if new_value != original_value:
199
+ modified_fields.add(field.name)
200
+
201
+ return modified_fields
@@ -1,5 +1,10 @@
1
1
  from django.db import models, transaction, connections
2
- from django.db.models import AutoField
2
+ from django.db.models import AutoField, Q, Max
3
+ from django.db import NotSupportedError
4
+ from django.db.models.constants import OnConflict
5
+ from django.db.models.expressions import DatabaseDefault
6
+ import operator
7
+ from functools import reduce
3
8
 
4
9
  from django_bulk_hooks import engine
5
10
  from django_bulk_hooks.constants import (
@@ -14,14 +19,120 @@ from django_bulk_hooks.constants import (
14
19
  VALIDATE_UPDATE,
15
20
  )
16
21
  from django_bulk_hooks.context import HookContext
17
- from django_bulk_hooks.queryset import HookQuerySet
18
22
 
19
23
 
20
- class BulkHookManager(models.Manager):
24
+ class HookQuerySet(models.QuerySet):
21
25
  CHUNK_SIZE = 200
22
26
 
23
- def get_queryset(self):
24
- return HookQuerySet(self.model, using=self._db)
27
+ @transaction.atomic
28
+ def delete(self):
29
+ objs = list(self)
30
+ if not objs:
31
+ return 0
32
+ return self.model.objects.bulk_delete(objs)
33
+
34
+ @transaction.atomic
35
+ def update(self, **kwargs):
36
+ instances = list(self)
37
+ if not instances:
38
+ return 0
39
+
40
+ model_cls = self.model
41
+ pks = [obj.pk for obj in instances]
42
+
43
+ # Load originals for hook comparison and ensure they match the order of instances
44
+ original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
45
+ originals = [original_map.get(obj.pk) for obj in instances]
46
+
47
+ # Apply field updates to instances
48
+ for obj in instances:
49
+ for field, value in kwargs.items():
50
+ setattr(obj, field, value)
51
+
52
+ # Run BEFORE_UPDATE hooks
53
+ ctx = HookContext(model_cls)
54
+ engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
55
+
56
+ # Use Django's built-in update logic directly
57
+ queryset = self.model.objects.filter(pk__in=pks)
58
+ update_count = queryset.update(**kwargs)
59
+
60
+ # Run AFTER_UPDATE hooks
61
+ engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
62
+
63
+ return update_count
64
+
65
+ @transaction.atomic
66
+ def bulk_create(
67
+ self,
68
+ objs,
69
+ batch_size=None,
70
+ ignore_conflicts=False,
71
+ update_conflicts=False,
72
+ update_fields=None,
73
+ unique_fields=None,
74
+ bypass_hooks=False,
75
+ bypass_validation=False,
76
+ ):
77
+ """
78
+ Insert each of the instances into the database. Behaves like Django's bulk_create,
79
+ but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
80
+ passed through to the correct logic. For MTI, only a subset of options may be supported.
81
+ """
82
+ model_cls = self.model
83
+
84
+ if batch_size is not None and batch_size <= 0:
85
+ raise ValueError("Batch size must be a positive integer.")
86
+
87
+ # Check for MTI - if we detect multi-table inheritance, we need special handling
88
+ is_mti = False
89
+ for parent in model_cls._meta.all_parents:
90
+ if parent._meta.concrete_model is not model_cls._meta.concrete_model:
91
+ is_mti = True
92
+ break
93
+
94
+ if not objs:
95
+ return objs
96
+
97
+ if any(not isinstance(obj, model_cls) for obj in objs):
98
+ raise TypeError(
99
+ f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
100
+ )
101
+
102
+ # Fire hooks before DB ops
103
+ if not bypass_hooks:
104
+ ctx = HookContext(model_cls)
105
+ if not bypass_validation:
106
+ engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
107
+ engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
108
+
109
+ # For MTI models, we need to handle them specially
110
+ if is_mti:
111
+ # Use our MTI-specific logic
112
+ result = self._mti_bulk_create(
113
+ objs,
114
+ batch_size=batch_size,
115
+ ignore_conflicts=ignore_conflicts,
116
+ update_conflicts=update_conflicts,
117
+ update_fields=update_fields,
118
+ unique_fields=unique_fields,
119
+ )
120
+ else:
121
+ # For single-table models, use Django's built-in bulk_create
122
+ # but we need to call it on the base manager to avoid recursion
123
+ result = model_cls._base_manager.bulk_create(
124
+ objs,
125
+ batch_size=batch_size,
126
+ ignore_conflicts=ignore_conflicts,
127
+ update_conflicts=update_conflicts,
128
+ update_fields=update_fields,
129
+ unique_fields=unique_fields,
130
+ )
131
+
132
+ if not bypass_hooks:
133
+ engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
134
+
135
+ return result
25
136
 
26
137
  @transaction.atomic
27
138
  def bulk_update(
@@ -65,7 +176,7 @@ class BulkHookManager(models.Manager):
65
176
  for i in range(0, len(objs), self.CHUNK_SIZE):
66
177
  chunk = objs[i : i + self.CHUNK_SIZE]
67
178
  # Call the base implementation to avoid re-triggering this method
68
- super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
179
+ super().bulk_update(chunk, fields, **kwargs)
69
180
 
70
181
  if not bypass_hooks:
71
182
  engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
@@ -73,116 +184,39 @@ class BulkHookManager(models.Manager):
73
184
  return objs
74
185
 
75
186
  @transaction.atomic
76
- def bulk_create(
77
- self,
78
- objs,
79
- batch_size=None,
80
- ignore_conflicts=False,
81
- update_conflicts=False,
82
- update_fields=None,
83
- unique_fields=None,
84
- bypass_hooks=False,
85
- bypass_validation=False,
86
- ):
87
- """
88
- Insert each of the instances into the database. Behaves like Django's bulk_create,
89
- but supports multi-table inheritance (MTI) models. All arguments are supported and
90
- passed through to the correct logic. For MTI, only a subset of options may be supported.
91
- """
92
- model_cls = self.model
93
-
94
- if batch_size is not None and batch_size <= 0:
95
- raise ValueError("Batch size must be a positive integer.")
96
-
97
- # Check that the parents share the same concrete model with our model to detect inheritance pattern
98
- # (Do NOT raise for MTI, just skip the exception)
99
- for parent in model_cls._meta.all_parents:
100
- if parent._meta.concrete_model is not model_cls._meta.concrete_model:
101
- # Do not raise, just continue
102
- break
103
-
187
+ def bulk_delete(self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False):
104
188
  if not objs:
105
- return objs
189
+ return []
190
+
191
+ model_cls = self.model
106
192
 
107
193
  if any(not isinstance(obj, model_cls) for obj in objs):
108
194
  raise TypeError(
109
- f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
195
+ f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
110
196
  )
111
197
 
112
- # Set auto_now_add/auto_now fields before DB ops
113
- self._set_auto_now_fields(objs, model_cls)
198
+ ctx = HookContext(model_cls)
114
199
 
115
- # Fire hooks before DB ops
116
200
  if not bypass_hooks:
117
- ctx = HookContext(model_cls)
201
+ # Run validation hooks first
118
202
  if not bypass_validation:
119
- engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
120
- engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
203
+ engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
121
204
 
122
- opts = model_cls._meta
123
- if unique_fields:
124
- unique_fields = [
125
- model_cls._meta.get_field(opts.pk.name if name == "pk" else name)
126
- for name in unique_fields
127
- ]
128
- if update_fields:
129
- update_fields = [model_cls._meta.get_field(name) for name in update_fields]
130
- on_conflict = self._check_bulk_create_options(
131
- ignore_conflicts,
132
- update_conflicts,
133
- update_fields,
134
- unique_fields,
135
- )
136
- self._for_write = True
137
- fields = [f for f in opts.concrete_fields if not f.generated]
138
- objs = list(objs)
139
- objs_with_pk, objs_without_pk = self._prepare_for_bulk_create(objs)
140
- with transaction.atomic(using=self.db, savepoint=False):
141
- self._handle_order_with_respect_to(objs)
142
- if objs_with_pk:
143
- returned_columns = self._batched_insert(
144
- objs_with_pk,
145
- fields,
146
- batch_size,
147
- on_conflict=on_conflict,
148
- update_fields=update_fields,
149
- unique_fields=unique_fields,
150
- )
151
- for obj_with_pk, results in zip(objs_with_pk, returned_columns):
152
- for result, field in zip(results, opts.db_returning_fields):
153
- if field != opts.pk:
154
- setattr(obj_with_pk, field.attname, result)
155
- for obj_with_pk in objs_with_pk:
156
- obj_with_pk._state.adding = False
157
- obj_with_pk._state.db = self.db
158
- if objs_without_pk:
159
- fields_wo_pk = [f for f in fields if not isinstance(f, AutoField)]
160
- returned_columns = self._batched_insert(
161
- objs_without_pk,
162
- fields_wo_pk,
163
- batch_size,
164
- on_conflict=on_conflict,
165
- update_fields=update_fields,
166
- unique_fields=unique_fields,
167
- )
168
- connection = connections[self.db]
169
- if (
170
- connection.features.can_return_rows_from_bulk_insert
171
- and on_conflict is None
172
- ):
173
- assert len(returned_columns) == len(objs_without_pk)
174
- for obj_without_pk, results in zip(objs_without_pk, returned_columns):
175
- for result, field in zip(results, opts.db_returning_fields):
176
- setattr(obj_without_pk, field.attname, result)
177
- obj_without_pk._state.adding = False
178
- obj_without_pk._state.db = self.db
205
+ # Then run business logic hooks
206
+ engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
207
+
208
+ pks = [obj.pk for obj in objs if obj.pk is not None]
209
+
210
+ # Use base manager for the actual deletion to prevent recursion
211
+ # The hooks have already been fired above, so we don't need them again
212
+ model_cls._base_manager.filter(pk__in=pks).delete()
179
213
 
180
214
  if not bypass_hooks:
181
- engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
215
+ engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
182
216
 
183
217
  return objs
184
218
 
185
- # --- Private helper methods (moved to bottom for clarity) ---
219
+ # --- Private helper methods ---
186
220
 
187
221
  def _detect_modified_fields(self, new_instances, original_instances):
188
222
  """
@@ -240,19 +274,19 @@ class BulkHookManager(models.Manager):
240
274
  chain.reverse()
241
275
  return chain
242
276
 
243
- def _mti_bulk_create(self, objs, inheritance_chain, **kwargs):
277
+ def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
244
278
  """
245
279
  Implements workaround: individual saves for parents, bulk create for child.
246
280
  Sets auto_now_add/auto_now fields for each model in the chain.
247
281
  """
282
+ if inheritance_chain is None:
283
+ inheritance_chain = self._get_inheritance_chain()
284
+
248
285
  batch_size = kwargs.get("batch_size") or len(objs)
249
286
  created_objects = []
250
287
  with transaction.atomic(using=self.db, savepoint=False):
251
288
  for i in range(0, len(objs), batch_size):
252
289
  batch = objs[i : i + batch_size]
253
- # Set auto_now fields for each model in the chain
254
- for model in inheritance_chain:
255
- self._set_auto_now_fields(batch, model)
256
290
  batch_result = self._process_mti_batch(
257
291
  batch, inheritance_chain, **kwargs
258
292
  )
@@ -284,8 +318,6 @@ class BulkHookManager(models.Manager):
284
318
  obj, child_model, parent_objects_map.get(id(obj), {})
285
319
  )
286
320
  child_objects.append(child_obj)
287
- # Handle order_with_respect_to like Django's bulk_create
288
- self._handle_order_with_respect_to(child_objects)
289
321
  # If the child model is still MTI, call our own logic recursively
290
322
  if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
291
323
  # Build inheritance chain for the child model
@@ -348,104 +380,3 @@ class BulkHookManager(models.Manager):
348
380
  if parent_link:
349
381
  setattr(child_obj, parent_link.name, parent_instance)
350
382
  return child_obj
351
-
352
- def _set_auto_now_fields(self, objs, model):
353
- """
354
- Set auto_now_add and auto_now fields on objects before bulk_create.
355
- """
356
- from django.utils import timezone
357
-
358
- now = timezone.now()
359
- for obj in objs:
360
- for field in model._meta.local_fields:
361
- if (
362
- getattr(field, "auto_now_add", False)
363
- and getattr(obj, field.name, None) is None
364
- ):
365
- setattr(obj, field.name, now)
366
- if getattr(field, "auto_now", False):
367
- setattr(obj, field.name, now)
368
-
369
- @transaction.atomic
370
- def bulk_delete(
371
- self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
372
- ):
373
- if not objs:
374
- return []
375
-
376
- model_cls = self.model
377
-
378
- if any(not isinstance(obj, model_cls) for obj in objs):
379
- raise TypeError(
380
- f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
381
- )
382
-
383
- ctx = HookContext(model_cls)
384
-
385
- if not bypass_hooks:
386
- # Run validation hooks first
387
- if not bypass_validation:
388
- engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
389
-
390
- # Then run business logic hooks
391
- engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
392
-
393
- pks = [obj.pk for obj in objs if obj.pk is not None]
394
-
395
- # Use base manager for the actual deletion to prevent recursion
396
- # The hooks have already been fired above, so we don't need them again
397
- model_cls._base_manager.filter(pk__in=pks).delete()
398
-
399
- if not bypass_hooks:
400
- engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
401
-
402
- @transaction.atomic
403
- def update(self, **kwargs):
404
- objs = list(self.all())
405
- if not objs:
406
- return 0
407
- for key, value in kwargs.items():
408
- for obj in objs:
409
- setattr(obj, key, value)
410
- self.bulk_update(objs, fields=list(kwargs.keys()))
411
- return len(objs)
412
-
413
- @transaction.atomic
414
- def delete(self):
415
- objs = list(self.all())
416
- if not objs:
417
- return 0
418
- self.bulk_delete(objs)
419
- return len(objs)
420
-
421
- @transaction.atomic
422
- def save(self, obj):
423
- if obj.pk:
424
- self.bulk_update(
425
- [obj],
426
- fields=[field.name for field in obj._meta.fields if field.name != "id"],
427
- )
428
- else:
429
- self.bulk_create([obj])
430
- return obj
431
-
432
- def _handle_order_with_respect_to(self, objs):
433
- """
434
- Set _order fields for models with order_with_respect_to.
435
- """
436
- for obj in objs:
437
- order_with_respect_to = obj.__class__._meta.order_with_respect_to
438
- if order_with_respect_to:
439
- key = getattr(obj, order_with_respect_to.attname)
440
- obj._order = key
441
- # Group by the value of order_with_respect_to
442
- groups = defaultdict(list)
443
- for obj in objs:
444
- order_with_respect_to = obj.__class__._meta.order_with_respect_to
445
- if order_with_respect_to:
446
- key = getattr(obj, order_with_respect_to.attname)
447
- groups[key].append(obj)
448
- # Enumerate within each group
449
- for group_objs in groups.values():
450
- for i, obj in enumerate(group_objs):
451
- obj._order = i
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.118"
3
+ version = "0.1.119"
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"
@@ -1,44 +0,0 @@
1
- from django.db import models, transaction
2
-
3
-
4
- class HookQuerySet(models.QuerySet):
5
- @transaction.atomic
6
- def delete(self):
7
- objs = list(self)
8
- if not objs:
9
- return 0
10
- return self.model.objects.bulk_delete(objs)
11
-
12
- @transaction.atomic
13
- def update(self, **kwargs):
14
- instances = list(self)
15
- if not instances:
16
- return 0
17
-
18
- model_cls = self.model
19
- pks = [obj.pk for obj in instances]
20
-
21
- # Load originals for hook comparison and ensure they match the order of instances
22
- original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
23
- originals = [original_map.get(obj.pk) for obj in instances]
24
-
25
- # Apply field updates to instances
26
- for obj in instances:
27
- for field, value in kwargs.items():
28
- setattr(obj, field, value)
29
-
30
- # Run BEFORE_UPDATE hooks
31
- from django_bulk_hooks import engine
32
- from django_bulk_hooks.context import HookContext
33
-
34
- ctx = HookContext(model_cls)
35
- engine.run(model_cls, "before_update", instances, originals, ctx=ctx)
36
-
37
- # Use Django's built-in update logic directly
38
- queryset = self.model.objects.filter(pk__in=pks)
39
- update_count = queryset.update(**kwargs)
40
-
41
- # Run AFTER_UPDATE hooks
42
- engine.run(model_cls, "after_update", instances, originals, ctx=ctx)
43
-
44
- return update_count