django-bulk-hooks 0.1.102__py3-none-any.whl → 0.1.103__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 +3 -49
- django_bulk_hooks/conditions.py +66 -261
- django_bulk_hooks/decorators.py +3 -48
- django_bulk_hooks/engine.py +9 -37
- django_bulk_hooks/handler.py +1 -1
- django_bulk_hooks/manager.py +39 -170
- django_bulk_hooks/models.py +35 -66
- django_bulk_hooks/priority.py +16 -0
- django_bulk_hooks/queryset.py +3 -2
- django_bulk_hooks/registry.py +3 -2
- django_bulk_hooks-0.1.103.dist-info/METADATA +217 -0
- django_bulk_hooks-0.1.103.dist-info/RECORD +17 -0
- django_bulk_hooks-0.1.102.dist-info/METADATA +0 -228
- django_bulk_hooks-0.1.102.dist-info/RECORD +0 -16
- {django_bulk_hooks-0.1.102.dist-info → django_bulk_hooks-0.1.103.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.102.dist-info → django_bulk_hooks-0.1.103.dist-info}/WHEEL +0 -0
django_bulk_hooks/manager.py
CHANGED
|
@@ -17,113 +17,10 @@ from django_bulk_hooks.queryset import HookQuerySet
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class BulkHookManager(models.Manager):
|
|
20
|
-
|
|
21
|
-
DEFAULT_CHUNK_SIZE = 200
|
|
22
|
-
DEFAULT_RELATED_CHUNK_SIZE = 500 # Higher for related object fetching
|
|
23
|
-
|
|
24
|
-
def __init__(self):
|
|
25
|
-
super().__init__()
|
|
26
|
-
self._chunk_size = self.DEFAULT_CHUNK_SIZE
|
|
27
|
-
self._related_chunk_size = self.DEFAULT_RELATED_CHUNK_SIZE
|
|
28
|
-
self._prefetch_related_fields = set()
|
|
29
|
-
self._select_related_fields = set()
|
|
30
|
-
|
|
31
|
-
def configure(
|
|
32
|
-
self,
|
|
33
|
-
chunk_size=None,
|
|
34
|
-
related_chunk_size=None,
|
|
35
|
-
select_related=None,
|
|
36
|
-
prefetch_related=None,
|
|
37
|
-
):
|
|
38
|
-
"""
|
|
39
|
-
Configure bulk operation parameters for this manager.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
chunk_size: Number of objects to process in each bulk operation chunk
|
|
43
|
-
related_chunk_size: Number of objects to fetch in each related object query
|
|
44
|
-
select_related: List of fields to always select_related in bulk operations
|
|
45
|
-
prefetch_related: List of fields to always prefetch_related in bulk operations
|
|
46
|
-
"""
|
|
47
|
-
if chunk_size is not None:
|
|
48
|
-
self._chunk_size = chunk_size
|
|
49
|
-
if related_chunk_size is not None:
|
|
50
|
-
self._related_chunk_size = related_chunk_size
|
|
51
|
-
if select_related:
|
|
52
|
-
self._select_related_fields.update(select_related)
|
|
53
|
-
if prefetch_related:
|
|
54
|
-
self._prefetch_related_fields.update(prefetch_related)
|
|
55
|
-
|
|
56
|
-
def _load_originals_optimized(self, pks, fields_to_fetch=None):
|
|
57
|
-
"""
|
|
58
|
-
Optimized loading of original instances with smart batching and field selection.
|
|
59
|
-
"""
|
|
60
|
-
queryset = self.model.objects.filter(pk__in=pks)
|
|
61
|
-
|
|
62
|
-
# Only select specific fields if provided and not empty
|
|
63
|
-
if fields_to_fetch and len(fields_to_fetch) > 0:
|
|
64
|
-
queryset = queryset.only("pk", *fields_to_fetch)
|
|
65
|
-
|
|
66
|
-
# Apply configured related field optimizations
|
|
67
|
-
if self._select_related_fields:
|
|
68
|
-
queryset = queryset.select_related(*self._select_related_fields)
|
|
69
|
-
if self._prefetch_related_fields:
|
|
70
|
-
queryset = queryset.prefetch_related(*self._prefetch_related_fields)
|
|
71
|
-
|
|
72
|
-
# Batch load in chunks to avoid memory issues
|
|
73
|
-
all_originals = []
|
|
74
|
-
for i in range(0, len(pks), self._related_chunk_size):
|
|
75
|
-
chunk_pks = pks[i : i + self._related_chunk_size]
|
|
76
|
-
chunk_originals = list(queryset.filter(pk__in=chunk_pks))
|
|
77
|
-
all_originals.extend(chunk_originals)
|
|
20
|
+
CHUNK_SIZE = 200
|
|
78
21
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _get_fields_to_fetch(self, objs, fields):
|
|
82
|
-
"""
|
|
83
|
-
Determine which fields need to be fetched based on what's being updated
|
|
84
|
-
and what's needed for hooks.
|
|
85
|
-
"""
|
|
86
|
-
fields_to_fetch = set(fields)
|
|
87
|
-
|
|
88
|
-
# Add fields needed by registered hooks
|
|
89
|
-
from django_bulk_hooks.registry import get_hooks
|
|
90
|
-
|
|
91
|
-
hooks = get_hooks(self.model, "before_update") + get_hooks(
|
|
92
|
-
self.model, "after_update"
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
for handler_cls, method_name, condition, _ in hooks:
|
|
96
|
-
if condition:
|
|
97
|
-
# If there's a condition, we need all fields it might access
|
|
98
|
-
fields_to_fetch.update(condition.get_required_fields())
|
|
99
|
-
|
|
100
|
-
# Filter out fields that don't exist on the model
|
|
101
|
-
valid_fields = set()
|
|
102
|
-
invalid_fields = set()
|
|
103
|
-
for field_name in fields_to_fetch:
|
|
104
|
-
try:
|
|
105
|
-
self.model._meta.get_field(field_name)
|
|
106
|
-
valid_fields.add(field_name)
|
|
107
|
-
except Exception as e:
|
|
108
|
-
# Field doesn't exist, skip it
|
|
109
|
-
invalid_fields.add(field_name)
|
|
110
|
-
import logging
|
|
111
|
-
|
|
112
|
-
logger = logging.getLogger(__name__)
|
|
113
|
-
logger.debug(
|
|
114
|
-
f"Field '{field_name}' requested by hook condition but doesn't exist on {self.model.__name__}: {e}"
|
|
115
|
-
)
|
|
116
|
-
continue
|
|
117
|
-
|
|
118
|
-
if invalid_fields:
|
|
119
|
-
import logging
|
|
120
|
-
logger = logging.getLogger(__name__)
|
|
121
|
-
logger.warning(
|
|
122
|
-
f"Invalid fields requested for {self.model.__name__}: {invalid_fields}. "
|
|
123
|
-
f"These fields were ignored to prevent errors."
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
return valid_fields
|
|
22
|
+
def get_queryset(self):
|
|
23
|
+
return HookQuerySet(self.model, using=self._db)
|
|
127
24
|
|
|
128
25
|
@transaction.atomic
|
|
129
26
|
def bulk_update(
|
|
@@ -140,42 +37,36 @@ class BulkHookManager(models.Manager):
|
|
|
140
37
|
)
|
|
141
38
|
|
|
142
39
|
if not bypass_hooks:
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
originals = self._load_originals_optimized(pks, fields_to_fetch)
|
|
149
|
-
|
|
150
|
-
# Create a mapping for quick lookup
|
|
151
|
-
original_map = {obj.pk: obj for obj in originals}
|
|
152
|
-
|
|
153
|
-
# Align originals with new instances
|
|
154
|
-
aligned_originals = [original_map.get(obj.pk) for obj in objs]
|
|
40
|
+
# Load originals for hook comparison and ensure they match the order of new instances
|
|
41
|
+
original_map = {
|
|
42
|
+
obj.pk: obj for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
|
|
43
|
+
}
|
|
44
|
+
originals = [original_map.get(obj.pk) for obj in objs]
|
|
155
45
|
|
|
156
46
|
ctx = HookContext(model_cls)
|
|
157
47
|
|
|
158
48
|
# Run validation hooks first
|
|
159
49
|
if not bypass_validation:
|
|
160
|
-
engine.run(model_cls, VALIDATE_UPDATE, objs,
|
|
50
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
161
51
|
|
|
162
52
|
# Then run business logic hooks
|
|
163
|
-
engine.run(model_cls, BEFORE_UPDATE, objs,
|
|
53
|
+
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
164
54
|
|
|
165
55
|
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
166
|
-
modified_fields = self._detect_modified_fields(objs,
|
|
56
|
+
modified_fields = self._detect_modified_fields(objs, originals)
|
|
167
57
|
if modified_fields:
|
|
58
|
+
# Convert to set for efficient union operation
|
|
168
59
|
fields_set = set(fields)
|
|
169
60
|
fields_set.update(modified_fields)
|
|
170
61
|
fields = list(fields_set)
|
|
171
62
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
63
|
+
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
64
|
+
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
65
|
+
# Call the base implementation to avoid re-triggering this method
|
|
175
66
|
super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
|
|
176
67
|
|
|
177
68
|
if not bypass_hooks:
|
|
178
|
-
engine.run(model_cls, AFTER_UPDATE, objs,
|
|
69
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
179
70
|
|
|
180
71
|
return objs
|
|
181
72
|
|
|
@@ -187,17 +78,11 @@ class BulkHookManager(models.Manager):
|
|
|
187
78
|
if not original_instances:
|
|
188
79
|
return set()
|
|
189
80
|
|
|
190
|
-
# Create a mapping of pk to original instance for efficient lookup
|
|
191
|
-
original_map = {obj.pk: obj for obj in original_instances if obj.pk is not None}
|
|
192
|
-
|
|
193
81
|
modified_fields = set()
|
|
194
82
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
original = original_map.get(new_instance.pk)
|
|
200
|
-
if not original:
|
|
83
|
+
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
84
|
+
for new_instance, original in zip(new_instances, original_instances):
|
|
85
|
+
if new_instance.pk is None or original is None:
|
|
201
86
|
continue
|
|
202
87
|
|
|
203
88
|
# Compare all fields to detect changes
|
|
@@ -224,42 +109,31 @@ class BulkHookManager(models.Manager):
|
|
|
224
109
|
|
|
225
110
|
@transaction.atomic
|
|
226
111
|
def bulk_create(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
227
|
-
if not objs:
|
|
228
|
-
return []
|
|
229
|
-
|
|
230
112
|
model_cls = self.model
|
|
231
|
-
result = []
|
|
232
113
|
|
|
233
114
|
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
234
115
|
raise TypeError(
|
|
235
116
|
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
236
117
|
)
|
|
237
118
|
|
|
119
|
+
result = []
|
|
120
|
+
|
|
238
121
|
if not bypass_hooks:
|
|
239
122
|
ctx = HookContext(model_cls)
|
|
240
123
|
|
|
241
|
-
#
|
|
124
|
+
# Run validation hooks first
|
|
242
125
|
if not bypass_validation:
|
|
243
|
-
|
|
244
|
-
chunk = objs[i : i + self._chunk_size]
|
|
245
|
-
engine.run(model_cls, VALIDATE_CREATE, chunk, ctx=ctx)
|
|
126
|
+
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
246
127
|
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
chunk = objs[i : i + self._chunk_size]
|
|
250
|
-
engine.run(model_cls, BEFORE_CREATE, chunk, ctx=ctx)
|
|
128
|
+
# Then run business logic hooks
|
|
129
|
+
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
251
130
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
created_chunk = super(models.Manager, self).bulk_create(chunk, **kwargs)
|
|
256
|
-
result.extend(created_chunk)
|
|
131
|
+
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
132
|
+
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
133
|
+
result.extend(super(models.Manager, self).bulk_create(chunk, **kwargs))
|
|
257
134
|
|
|
258
135
|
if not bypass_hooks:
|
|
259
|
-
|
|
260
|
-
for i in range(0, len(result), self._chunk_size):
|
|
261
|
-
chunk = result[i : i + self._chunk_size]
|
|
262
|
-
engine.run(model_cls, AFTER_CREATE, chunk, ctx=ctx)
|
|
136
|
+
engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
|
|
263
137
|
|
|
264
138
|
return result
|
|
265
139
|
|
|
@@ -271,7 +145,6 @@ class BulkHookManager(models.Manager):
|
|
|
271
145
|
return []
|
|
272
146
|
|
|
273
147
|
model_cls = self.model
|
|
274
|
-
chunk_size = batch_size or self._chunk_size
|
|
275
148
|
|
|
276
149
|
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
277
150
|
raise TypeError(
|
|
@@ -281,25 +154,21 @@ class BulkHookManager(models.Manager):
|
|
|
281
154
|
ctx = HookContext(model_cls)
|
|
282
155
|
|
|
283
156
|
if not bypass_hooks:
|
|
284
|
-
#
|
|
285
|
-
|
|
286
|
-
|
|
157
|
+
# Run validation hooks first
|
|
158
|
+
if not bypass_validation:
|
|
159
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
287
160
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
engine.run(model_cls, BEFORE_DELETE, chunk, ctx=ctx)
|
|
161
|
+
# Then run business logic hooks
|
|
162
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
291
163
|
|
|
292
|
-
# Collect PKs and delete in chunks
|
|
293
164
|
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
165
|
+
|
|
166
|
+
# Use base manager for the actual deletion to prevent recursion
|
|
167
|
+
# The hooks have already been fired above, so we don't need them again
|
|
168
|
+
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
297
169
|
|
|
298
170
|
if not bypass_hooks:
|
|
299
|
-
|
|
300
|
-
for i in range(0, len(objs), chunk_size):
|
|
301
|
-
chunk = objs[i : i + chunk_size]
|
|
302
|
-
engine.run(model_cls, AFTER_DELETE, chunk, ctx=ctx)
|
|
171
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
303
172
|
|
|
304
173
|
return objs
|
|
305
174
|
|
django_bulk_hooks/models.py
CHANGED
|
@@ -14,35 +14,6 @@ from django_bulk_hooks.constants import (
|
|
|
14
14
|
from django_bulk_hooks.context import HookContext
|
|
15
15
|
from django_bulk_hooks.engine import run
|
|
16
16
|
from django_bulk_hooks.manager import BulkHookManager
|
|
17
|
-
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
|
|
18
|
-
from functools import wraps
|
|
19
|
-
import contextlib
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@contextlib.contextmanager
|
|
23
|
-
def patch_foreign_key_behavior():
|
|
24
|
-
"""
|
|
25
|
-
Temporarily patches Django's foreign key descriptor to return None instead of raising
|
|
26
|
-
RelatedObjectDoesNotExist when accessing an unset foreign key field.
|
|
27
|
-
"""
|
|
28
|
-
original_get = ForwardManyToOneDescriptor.__get__
|
|
29
|
-
|
|
30
|
-
@wraps(original_get)
|
|
31
|
-
def safe_get(self, instance, cls=None):
|
|
32
|
-
if instance is None:
|
|
33
|
-
return self
|
|
34
|
-
try:
|
|
35
|
-
return original_get(self, instance, cls)
|
|
36
|
-
except self.RelatedObjectDoesNotExist:
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
# Patch the descriptor
|
|
40
|
-
ForwardManyToOneDescriptor.__get__ = safe_get
|
|
41
|
-
try:
|
|
42
|
-
yield
|
|
43
|
-
finally:
|
|
44
|
-
# Restore original behavior
|
|
45
|
-
ForwardManyToOneDescriptor.__get__ = original_get
|
|
46
17
|
|
|
47
18
|
|
|
48
19
|
class HookModelMixin(models.Model):
|
|
@@ -57,70 +28,68 @@ class HookModelMixin(models.Model):
|
|
|
57
28
|
This ensures that when Django calls clean() (like in admin forms),
|
|
58
29
|
it triggers the VALIDATE_* hooks for validation only.
|
|
59
30
|
"""
|
|
60
|
-
# Call Django's clean first
|
|
61
31
|
super().clean()
|
|
62
32
|
|
|
63
|
-
# Skip hook validation during admin form validation
|
|
64
|
-
# This prevents RelatedObjectDoesNotExist errors when Django hasn't
|
|
65
|
-
# fully set up the object's relationships yet
|
|
66
|
-
if hasattr(self, '_state') and getattr(self._state, 'validating', False):
|
|
67
|
-
return
|
|
68
|
-
|
|
69
33
|
# Determine if this is a create or update operation
|
|
70
34
|
is_create = self.pk is None
|
|
71
35
|
|
|
72
36
|
if is_create:
|
|
73
37
|
# For create operations, run VALIDATE_CREATE hooks for validation
|
|
74
38
|
ctx = HookContext(self.__class__)
|
|
75
|
-
|
|
76
|
-
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
39
|
+
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
77
40
|
else:
|
|
78
41
|
# For update operations, run VALIDATE_UPDATE hooks for validation
|
|
79
42
|
try:
|
|
80
43
|
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
81
44
|
ctx = HookContext(self.__class__)
|
|
82
|
-
|
|
83
|
-
run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
45
|
+
run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
84
46
|
except self.__class__.DoesNotExist:
|
|
85
47
|
# If the old instance doesn't exist, treat as create
|
|
86
48
|
ctx = HookContext(self.__class__)
|
|
87
|
-
|
|
88
|
-
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
49
|
+
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
89
50
|
|
|
90
51
|
def save(self, *args, **kwargs):
|
|
91
52
|
is_create = self.pk is None
|
|
92
|
-
ctx = HookContext(self.__class__)
|
|
93
53
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
54
|
+
if is_create:
|
|
55
|
+
# For create operations, we don't have old records
|
|
56
|
+
ctx = HookContext(self.__class__)
|
|
57
|
+
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
58
|
+
|
|
59
|
+
super().save(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
62
|
+
else:
|
|
63
|
+
# For update operations, we need to get the old record
|
|
64
|
+
try:
|
|
65
|
+
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
66
|
+
ctx = HookContext(self.__class__)
|
|
67
|
+
run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
68
|
+
|
|
69
|
+
super().save(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
72
|
+
except self.__class__.DoesNotExist:
|
|
73
|
+
# If the old instance doesn't exist, treat as create
|
|
74
|
+
ctx = HookContext(self.__class__)
|
|
98
75
|
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
76
|
+
|
|
99
77
|
super().save(*args, **kwargs)
|
|
78
|
+
|
|
100
79
|
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
101
|
-
else:
|
|
102
|
-
# For update operations
|
|
103
|
-
try:
|
|
104
|
-
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
105
|
-
run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
106
|
-
super().save(*args, **kwargs)
|
|
107
|
-
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
108
|
-
except self.__class__.DoesNotExist:
|
|
109
|
-
# If the old instance doesn't exist, treat as create
|
|
110
|
-
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
111
|
-
super().save(*args, **kwargs)
|
|
112
|
-
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
113
80
|
|
|
114
81
|
return self
|
|
115
82
|
|
|
116
83
|
def delete(self, *args, **kwargs):
|
|
117
84
|
ctx = HookContext(self.__class__)
|
|
118
85
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
86
|
+
# Run validation hooks first
|
|
87
|
+
run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
|
|
88
|
+
|
|
89
|
+
# Then run business logic hooks
|
|
90
|
+
run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
|
|
91
|
+
|
|
92
|
+
result = super().delete(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
|
|
126
95
|
return result
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Priority(IntEnum):
|
|
5
|
+
"""
|
|
6
|
+
Named priorities for django-bulk-hooks hooks.
|
|
7
|
+
|
|
8
|
+
Lower values run earlier (higher priority).
|
|
9
|
+
Hooks are sorted in ascending order.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
HIGHEST = 0 # runs first
|
|
13
|
+
HIGH = 25 # runs early
|
|
14
|
+
NORMAL = 50 # default ordering
|
|
15
|
+
LOW = 75 # runs later
|
|
16
|
+
LOWEST = 100 # runs last
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -18,8 +18,9 @@ class HookQuerySet(models.QuerySet):
|
|
|
18
18
|
model_cls = self.model
|
|
19
19
|
pks = [obj.pk for obj in instances]
|
|
20
20
|
|
|
21
|
-
# Load originals for hook comparison
|
|
22
|
-
|
|
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]
|
|
23
24
|
|
|
24
25
|
# Apply field updates to instances
|
|
25
26
|
for obj in instances:
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from typing import Union
|
|
3
3
|
|
|
4
|
-
from django_bulk_hooks.
|
|
4
|
+
from django_bulk_hooks.priority import Priority
|
|
5
5
|
|
|
6
6
|
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
|
|
7
7
|
|
|
@@ -17,7 +17,8 @@ def register_hook(
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def get_hooks(model, event):
|
|
20
|
-
|
|
20
|
+
hooks = _hooks.get((model, event), [])
|
|
21
|
+
return hooks
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def list_all_hooks():
|