django-bulk-hooks 0.1.101__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 -61
- django_bulk_hooks/conditions.py +100 -277
- django_bulk_hooks/decorators.py +70 -10
- django_bulk_hooks/engine.py +20 -115
- django_bulk_hooks/handler.py +4 -29
- django_bulk_hooks/manager.py +48 -141
- django_bulk_hooks/models.py +35 -85
- django_bulk_hooks/priority.py +16 -0
- django_bulk_hooks/queryset.py +3 -2
- django_bulk_hooks/registry.py +6 -5
- 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.101.dist-info/METADATA +0 -295
- django_bulk_hooks-0.1.101.dist-info/RECORD +0 -16
- {django_bulk_hooks-0.1.101.dist-info → django_bulk_hooks-0.1.103.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.101.dist-info → django_bulk_hooks-0.1.103.dist-info}/WHEEL +0 -0
django_bulk_hooks/__init__.py
CHANGED
|
@@ -1,62 +1,4 @@
|
|
|
1
|
-
from django_bulk_hooks.
|
|
2
|
-
|
|
3
|
-
HasChanged,
|
|
4
|
-
IsBlank,
|
|
5
|
-
IsEqual,
|
|
6
|
-
IsGreaterThan,
|
|
7
|
-
IsGreaterThanOrEqual,
|
|
8
|
-
IsLessThan,
|
|
9
|
-
IsLessThanOrEqual,
|
|
10
|
-
IsNotEqual,
|
|
11
|
-
LambdaCondition,
|
|
12
|
-
WasEqual,
|
|
13
|
-
is_field_set,
|
|
14
|
-
safe_get_related_attr,
|
|
15
|
-
safe_get_related_object,
|
|
16
|
-
)
|
|
17
|
-
from django_bulk_hooks.constants import (
|
|
18
|
-
AFTER_CREATE,
|
|
19
|
-
AFTER_DELETE,
|
|
20
|
-
AFTER_UPDATE,
|
|
21
|
-
BEFORE_CREATE,
|
|
22
|
-
BEFORE_DELETE,
|
|
23
|
-
BEFORE_UPDATE,
|
|
24
|
-
VALIDATE_CREATE,
|
|
25
|
-
VALIDATE_DELETE,
|
|
26
|
-
VALIDATE_UPDATE,
|
|
27
|
-
)
|
|
28
|
-
from django_bulk_hooks.decorators import hook, select_related
|
|
29
|
-
from django_bulk_hooks.enums import Priority
|
|
30
|
-
from django_bulk_hooks.handler import HookHandler
|
|
31
|
-
from django_bulk_hooks.models import HookModelMixin
|
|
1
|
+
from django_bulk_hooks.handler import Hook
|
|
2
|
+
from django_bulk_hooks.manager import BulkHookManager
|
|
32
3
|
|
|
33
|
-
__all__ = [
|
|
34
|
-
"HookHandler",
|
|
35
|
-
"HookModelMixin",
|
|
36
|
-
"BEFORE_CREATE",
|
|
37
|
-
"AFTER_CREATE",
|
|
38
|
-
"BEFORE_UPDATE",
|
|
39
|
-
"AFTER_UPDATE",
|
|
40
|
-
"BEFORE_DELETE",
|
|
41
|
-
"AFTER_DELETE",
|
|
42
|
-
"VALIDATE_CREATE",
|
|
43
|
-
"VALIDATE_UPDATE",
|
|
44
|
-
"VALIDATE_DELETE",
|
|
45
|
-
"safe_get_related_object",
|
|
46
|
-
"safe_get_related_attr",
|
|
47
|
-
"is_field_set",
|
|
48
|
-
"Priority",
|
|
49
|
-
"hook",
|
|
50
|
-
"select_related",
|
|
51
|
-
"ChangesTo",
|
|
52
|
-
"HasChanged",
|
|
53
|
-
"IsEqual",
|
|
54
|
-
"IsNotEqual",
|
|
55
|
-
"WasEqual",
|
|
56
|
-
"IsBlank",
|
|
57
|
-
"IsGreaterThan",
|
|
58
|
-
"IsLessThan",
|
|
59
|
-
"IsGreaterThanOrEqual",
|
|
60
|
-
"IsLessThanOrEqual",
|
|
61
|
-
"LambdaCondition",
|
|
62
|
-
]
|
|
4
|
+
__all__ = ["BulkHookManager", "Hook"]
|
django_bulk_hooks/conditions.py
CHANGED
|
@@ -1,93 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def safe_get_related_object(instance, field_name):
|
|
5
|
-
"""
|
|
6
|
-
Safely get a related object without raising RelatedObjectDoesNotExist.
|
|
7
|
-
Returns None if the foreign key field is None or the related object doesn't exist.
|
|
8
|
-
"""
|
|
9
|
-
if not hasattr(instance, field_name):
|
|
10
|
-
return None
|
|
11
|
-
|
|
12
|
-
# Get the foreign key field
|
|
13
|
-
try:
|
|
14
|
-
field = instance._meta.get_field(field_name)
|
|
15
|
-
if not field.is_relation or field.many_to_many or field.one_to_many:
|
|
16
|
-
return getattr(instance, field_name, None)
|
|
17
|
-
except models.FieldDoesNotExist:
|
|
18
|
-
return getattr(instance, field_name, None)
|
|
19
|
-
|
|
20
|
-
# Check if the foreign key field is None
|
|
21
|
-
fk_field_name = f"{field_name}_id"
|
|
22
|
-
if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
# Try to get the related object, but catch RelatedObjectDoesNotExist
|
|
26
|
-
try:
|
|
27
|
-
return getattr(instance, field_name)
|
|
28
|
-
except field.related_model.RelatedObjectDoesNotExist:
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def safe_get_related_attr(instance, field_name, attr_name=None):
|
|
33
|
-
"""
|
|
34
|
-
Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
|
|
35
|
-
|
|
36
|
-
This is particularly useful in hooks where objects might not have their related
|
|
37
|
-
fields populated yet (e.g., during bulk_create operations or on unsaved objects).
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
instance: The model instance
|
|
41
|
-
field_name: The foreign key field name
|
|
42
|
-
attr_name: Optional attribute name to access on the related object
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
The related object, the attribute value, or None if not available
|
|
46
|
-
|
|
47
|
-
Example:
|
|
48
|
-
# Instead of: loan_transaction.status.name (which might fail)
|
|
49
|
-
# Use: safe_get_related_attr(loan_transaction, 'status', 'name')
|
|
50
|
-
|
|
51
|
-
status_name = safe_get_related_attr(loan_transaction, 'status', 'name')
|
|
52
|
-
if status_name in {Status.COMPLETE.value, Status.FAILED.value}:
|
|
53
|
-
# Process the transaction
|
|
54
|
-
pass
|
|
55
|
-
"""
|
|
56
|
-
# For unsaved objects, check the foreign key ID field first
|
|
57
|
-
if instance.pk is None:
|
|
58
|
-
fk_field_name = f"{field_name}_id"
|
|
59
|
-
if hasattr(instance, fk_field_name):
|
|
60
|
-
fk_value = getattr(instance, fk_field_name, None)
|
|
61
|
-
if fk_value is None:
|
|
62
|
-
return None
|
|
63
|
-
# If we have an ID but the object isn't loaded, try to load it
|
|
64
|
-
try:
|
|
65
|
-
field = instance._meta.get_field(field_name)
|
|
66
|
-
if hasattr(field, "related_model"):
|
|
67
|
-
related_obj = field.related_model.objects.get(id=fk_value)
|
|
68
|
-
if attr_name is None:
|
|
69
|
-
return related_obj
|
|
70
|
-
return getattr(related_obj, attr_name, None)
|
|
71
|
-
except (field.related_model.DoesNotExist, AttributeError):
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
# For saved objects or when the above doesn't work, use the original method
|
|
75
|
-
related_obj = safe_get_related_object(instance, field_name)
|
|
76
|
-
if related_obj is None:
|
|
77
|
-
return None
|
|
78
|
-
if attr_name is None:
|
|
79
|
-
return related_obj
|
|
80
|
-
return getattr(related_obj, attr_name, None)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def is_field_set(instance, field_name):
|
|
1
|
+
def resolve_dotted_attr(instance, dotted_path):
|
|
84
2
|
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
This is useful for checking if a field has been explicitly set,
|
|
88
|
-
even if it's been set to None.
|
|
3
|
+
Recursively resolve a dotted attribute path, e.g., "type.category".
|
|
89
4
|
"""
|
|
90
|
-
|
|
5
|
+
for attr in dotted_path.split("."):
|
|
6
|
+
if instance is None:
|
|
7
|
+
return None
|
|
8
|
+
instance = getattr(instance, attr, None)
|
|
9
|
+
return instance
|
|
91
10
|
|
|
92
11
|
|
|
93
12
|
class HookCondition:
|
|
@@ -106,253 +25,157 @@ class HookCondition:
|
|
|
106
25
|
def __invert__(self):
|
|
107
26
|
return NotCondition(self)
|
|
108
27
|
|
|
109
|
-
def get_required_fields(self):
|
|
110
|
-
"""
|
|
111
|
-
Returns a set of field names that this condition needs to evaluate.
|
|
112
|
-
Override in subclasses to specify required fields.
|
|
113
|
-
"""
|
|
114
|
-
return set()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class IsBlank(HookCondition):
|
|
118
|
-
"""
|
|
119
|
-
Condition that checks if a field is blank (None or empty string).
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
def __init__(self, field_name):
|
|
123
|
-
self.field_name = field_name
|
|
124
|
-
|
|
125
|
-
def check(self, instance, original_instance=None):
|
|
126
|
-
value = getattr(instance, self.field_name, None)
|
|
127
|
-
return value is None or value == ""
|
|
128
|
-
|
|
129
|
-
def get_required_fields(self):
|
|
130
|
-
return {self.field_name}
|
|
131
28
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
self.
|
|
136
|
-
|
|
137
|
-
def check(self, instance, original_instance=None):
|
|
138
|
-
return all(c.check(instance, original_instance) for c in self.conditions)
|
|
139
|
-
|
|
140
|
-
def get_required_fields(self):
|
|
141
|
-
fields = set()
|
|
142
|
-
for condition in self.conditions:
|
|
143
|
-
fields.update(condition.get_required_fields())
|
|
144
|
-
return fields
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class OrCondition(HookCondition):
|
|
148
|
-
def __init__(self, *conditions):
|
|
149
|
-
self.conditions = conditions
|
|
150
|
-
|
|
151
|
-
def check(self, instance, original_instance=None):
|
|
152
|
-
return any(c.check(instance, original_instance) for c in self.conditions)
|
|
153
|
-
|
|
154
|
-
def get_required_fields(self):
|
|
155
|
-
fields = set()
|
|
156
|
-
for condition in self.conditions:
|
|
157
|
-
fields.update(condition.get_required_fields())
|
|
158
|
-
return fields
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class NotCondition(HookCondition):
|
|
162
|
-
def __init__(self, condition):
|
|
163
|
-
self.condition = condition
|
|
29
|
+
class IsNotEqual(HookCondition):
|
|
30
|
+
def __init__(self, field, value, only_on_change=False):
|
|
31
|
+
self.field = field
|
|
32
|
+
self.value = value
|
|
33
|
+
self.only_on_change = only_on_change
|
|
164
34
|
|
|
165
35
|
def check(self, instance, original_instance=None):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
36
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
37
|
+
if self.only_on_change:
|
|
38
|
+
if original_instance is None:
|
|
39
|
+
return False
|
|
40
|
+
previous = resolve_dotted_attr(original_instance, self.field)
|
|
41
|
+
return previous == self.value and current != self.value
|
|
42
|
+
else:
|
|
43
|
+
return current != self.value
|
|
170
44
|
|
|
171
45
|
|
|
172
46
|
class IsEqual(HookCondition):
|
|
173
|
-
def __init__(self,
|
|
174
|
-
self.
|
|
47
|
+
def __init__(self, field, value, only_on_change=False):
|
|
48
|
+
self.field = field
|
|
175
49
|
self.value = value
|
|
50
|
+
self.only_on_change = only_on_change
|
|
176
51
|
|
|
177
52
|
def check(self, instance, original_instance=None):
|
|
178
|
-
|
|
53
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
54
|
+
if self.only_on_change:
|
|
55
|
+
if original_instance is None:
|
|
56
|
+
return False
|
|
57
|
+
previous = resolve_dotted_attr(original_instance, self.field)
|
|
58
|
+
return previous != self.value and current == self.value
|
|
59
|
+
else:
|
|
60
|
+
return current == self.value
|
|
179
61
|
|
|
180
|
-
def get_required_fields(self):
|
|
181
|
-
return {self.field_name}
|
|
182
62
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
self.
|
|
187
|
-
self.value = value
|
|
63
|
+
class HasChanged(HookCondition):
|
|
64
|
+
def __init__(self, field, has_changed=True):
|
|
65
|
+
self.field = field
|
|
66
|
+
self.has_changed = has_changed
|
|
188
67
|
|
|
189
68
|
def check(self, instance, original_instance=None):
|
|
190
|
-
if original_instance
|
|
69
|
+
if not original_instance:
|
|
191
70
|
return False
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return {self.field_name}
|
|
71
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
72
|
+
previous = resolve_dotted_attr(original_instance, self.field)
|
|
73
|
+
return (current != previous) == self.has_changed
|
|
196
74
|
|
|
197
75
|
|
|
198
|
-
class
|
|
199
|
-
def __init__(self,
|
|
76
|
+
class WasEqual(HookCondition):
|
|
77
|
+
def __init__(self, field, value, only_on_change=False):
|
|
200
78
|
"""
|
|
201
|
-
Check if a field's value
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
field_name: The field name to check
|
|
205
|
-
has_changed: If True (default), condition passes when field has changed.
|
|
206
|
-
If False, condition passes when field has remained the same.
|
|
207
|
-
This is useful for:
|
|
208
|
-
- Detecting stable/unchanged fields
|
|
209
|
-
- Validating field immutability
|
|
210
|
-
- Ensuring critical fields remain constant
|
|
211
|
-
- State machine validations
|
|
79
|
+
Check if a field's original value was `value`.
|
|
80
|
+
If only_on_change is True, only return True when the field has changed away from that value.
|
|
212
81
|
"""
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
82
|
+
self.field = field
|
|
83
|
+
self.value = value
|
|
84
|
+
self.only_on_change = only_on_change
|
|
215
85
|
|
|
216
86
|
def check(self, instance, original_instance=None):
|
|
217
87
|
if original_instance is None:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return (current_value != original_value) == self.has_changed
|
|
226
|
-
|
|
227
|
-
def get_required_fields(self):
|
|
228
|
-
return {self.field_name}
|
|
88
|
+
return False
|
|
89
|
+
previous = resolve_dotted_attr(original_instance, self.field)
|
|
90
|
+
if self.only_on_change:
|
|
91
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
92
|
+
return previous == self.value and current != self.value
|
|
93
|
+
else:
|
|
94
|
+
return previous == self.value
|
|
229
95
|
|
|
230
96
|
|
|
231
97
|
class ChangesTo(HookCondition):
|
|
232
|
-
def __init__(self,
|
|
233
|
-
|
|
98
|
+
def __init__(self, field, value):
|
|
99
|
+
"""
|
|
100
|
+
Check if a field's value has changed to `value`.
|
|
101
|
+
Only returns True when original value != value and current value == value.
|
|
102
|
+
"""
|
|
103
|
+
self.field = field
|
|
234
104
|
self.value = value
|
|
235
105
|
|
|
236
106
|
def check(self, instance, original_instance=None):
|
|
237
107
|
if original_instance is None:
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def get_required_fields(self):
|
|
244
|
-
return {self.field_name}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class IsNotEqual(HookCondition):
|
|
248
|
-
def __init__(self, field_name, value):
|
|
249
|
-
self.field_name = field_name
|
|
250
|
-
self.value = value
|
|
251
|
-
|
|
252
|
-
def check(self, instance, original_instance=None):
|
|
253
|
-
return getattr(instance, self.field_name, None) != self.value
|
|
254
|
-
|
|
255
|
-
def get_required_fields(self):
|
|
256
|
-
return {self.field_name}
|
|
108
|
+
return False
|
|
109
|
+
previous = resolve_dotted_attr(original_instance, self.field)
|
|
110
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
111
|
+
return previous != self.value and current == self.value
|
|
257
112
|
|
|
258
113
|
|
|
259
114
|
class IsGreaterThan(HookCondition):
|
|
260
|
-
def __init__(self,
|
|
261
|
-
self.
|
|
115
|
+
def __init__(self, field, value):
|
|
116
|
+
self.field = field
|
|
262
117
|
self.value = value
|
|
263
118
|
|
|
264
119
|
def check(self, instance, original_instance=None):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return False
|
|
268
|
-
return field_value > self.value
|
|
269
|
-
|
|
270
|
-
def get_required_fields(self):
|
|
271
|
-
return {self.field_name}
|
|
120
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
121
|
+
return current is not None and current > self.value
|
|
272
122
|
|
|
273
123
|
|
|
274
|
-
class
|
|
275
|
-
def __init__(self,
|
|
276
|
-
self.
|
|
124
|
+
class IsGreaterThanOrEqual(HookCondition):
|
|
125
|
+
def __init__(self, field, value):
|
|
126
|
+
self.field = field
|
|
277
127
|
self.value = value
|
|
278
128
|
|
|
279
129
|
def check(self, instance, original_instance=None):
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return False
|
|
283
|
-
return field_value < self.value
|
|
130
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
131
|
+
return current is not None and current >= self.value
|
|
284
132
|
|
|
285
|
-
def get_required_fields(self):
|
|
286
|
-
return {self.field_name}
|
|
287
133
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
self.field_name = field_name
|
|
134
|
+
class IsLessThan(HookCondition):
|
|
135
|
+
def __init__(self, field, value):
|
|
136
|
+
self.field = field
|
|
292
137
|
self.value = value
|
|
293
138
|
|
|
294
139
|
def check(self, instance, original_instance=None):
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return False
|
|
298
|
-
return field_value >= self.value
|
|
299
|
-
|
|
300
|
-
def get_required_fields(self):
|
|
301
|
-
return {self.field_name}
|
|
140
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
141
|
+
return current is not None and current < self.value
|
|
302
142
|
|
|
303
143
|
|
|
304
144
|
class IsLessThanOrEqual(HookCondition):
|
|
305
|
-
def __init__(self,
|
|
306
|
-
self.
|
|
145
|
+
def __init__(self, field, value):
|
|
146
|
+
self.field = field
|
|
307
147
|
self.value = value
|
|
308
148
|
|
|
309
149
|
def check(self, instance, original_instance=None):
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return False
|
|
313
|
-
return field_value <= self.value
|
|
150
|
+
current = resolve_dotted_attr(instance, self.field)
|
|
151
|
+
return current is not None and current <= self.value
|
|
314
152
|
|
|
315
|
-
def get_required_fields(self):
|
|
316
|
-
return {self.field_name}
|
|
317
153
|
|
|
154
|
+
class AndCondition(HookCondition):
|
|
155
|
+
def __init__(self, cond1, cond2):
|
|
156
|
+
self.cond1 = cond1
|
|
157
|
+
self.cond2 = cond2
|
|
318
158
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
159
|
+
def check(self, instance, original_instance=None):
|
|
160
|
+
return self.cond1.check(instance, original_instance) and self.cond2.check(
|
|
161
|
+
instance, original_instance
|
|
162
|
+
)
|
|
322
163
|
|
|
323
|
-
This makes it easy to create custom conditions inline without defining
|
|
324
|
-
a full class.
|
|
325
164
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
165
|
+
class OrCondition(HookCondition):
|
|
166
|
+
def __init__(self, cond1, cond2):
|
|
167
|
+
self.cond1 = cond1
|
|
168
|
+
self.cond2 = cond2
|
|
329
169
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
170
|
+
def check(self, instance, original_instance=None):
|
|
171
|
+
return self.cond1.check(instance, original_instance) or self.cond2.check(
|
|
172
|
+
instance, original_instance
|
|
333
173
|
)
|
|
334
174
|
|
|
335
|
-
# Using in a hook
|
|
336
|
-
@hook(Product, "after_update", condition=LambdaCondition(
|
|
337
|
-
lambda instance: instance.price > 100 and instance.is_active
|
|
338
|
-
))
|
|
339
|
-
def handle_expensive_active_products(self, new_records, old_records):
|
|
340
|
-
pass
|
|
341
|
-
"""
|
|
342
|
-
|
|
343
|
-
def __init__(self, func, required_fields=None):
|
|
344
|
-
"""
|
|
345
|
-
Initialize with a callable function.
|
|
346
175
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"""
|
|
351
|
-
self.func = func
|
|
352
|
-
self._required_fields = required_fields or set()
|
|
176
|
+
class NotCondition(HookCondition):
|
|
177
|
+
def __init__(self, cond):
|
|
178
|
+
self.cond = cond
|
|
353
179
|
|
|
354
180
|
def check(self, instance, original_instance=None):
|
|
355
|
-
return self.
|
|
356
|
-
|
|
357
|
-
def get_required_fields(self):
|
|
358
|
-
return self._required_fields
|
|
181
|
+
return not self.cond.check(instance, original_instance)
|
django_bulk_hooks/decorators.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import wraps
|
|
|
4
4
|
from django.core.exceptions import FieldDoesNotExist
|
|
5
5
|
from django_bulk_hooks.enums import DEFAULT_PRIORITY
|
|
6
6
|
from django_bulk_hooks.registry import register_hook
|
|
7
|
-
from django_bulk_hooks.engine import safe_get_related_object
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
|
|
@@ -24,22 +23,83 @@ def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
|
|
|
24
23
|
|
|
25
24
|
def select_related(*related_fields):
|
|
26
25
|
"""
|
|
27
|
-
Decorator that
|
|
28
|
-
|
|
29
|
-
This decorator works in conjunction with the hook system to ensure that
|
|
30
|
-
related fields are bulk-loaded before the hook logic runs, preventing
|
|
31
|
-
queries in loops.
|
|
26
|
+
Decorator that preloads related fields in-place on `new_records`, before the hook logic runs.
|
|
32
27
|
|
|
33
28
|
- Works with instance methods (resolves `self`)
|
|
34
29
|
- Avoids replacing model instances
|
|
35
30
|
- Populates Django's relation cache to avoid extra queries
|
|
36
|
-
- Provides bulk loading for performance optimization
|
|
37
31
|
"""
|
|
38
32
|
|
|
39
33
|
def decorator(func):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
sig = inspect.signature(func)
|
|
35
|
+
|
|
36
|
+
@wraps(func)
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
38
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
39
|
+
bound.apply_defaults()
|
|
40
|
+
|
|
41
|
+
if "new_records" not in bound.arguments:
|
|
42
|
+
raise TypeError(
|
|
43
|
+
"@preload_related requires a 'new_records' argument in the decorated function"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
new_records = bound.arguments["new_records"]
|
|
47
|
+
|
|
48
|
+
if not isinstance(new_records, list):
|
|
49
|
+
raise TypeError(
|
|
50
|
+
f"@preload_related expects a list of model instances, got {type(new_records)}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if not new_records:
|
|
54
|
+
return func(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
# Determine which instances actually need preloading
|
|
57
|
+
model_cls = new_records[0].__class__
|
|
58
|
+
ids_to_fetch = []
|
|
59
|
+
for obj in new_records:
|
|
60
|
+
if obj.pk is None:
|
|
61
|
+
continue
|
|
62
|
+
# if any related field is not already cached on the instance,
|
|
63
|
+
# mark it for fetching
|
|
64
|
+
if any(field not in obj._state.fields_cache for field in related_fields):
|
|
65
|
+
ids_to_fetch.append(obj.pk)
|
|
66
|
+
|
|
67
|
+
fetched = {}
|
|
68
|
+
if ids_to_fetch:
|
|
69
|
+
fetched = model_cls.objects.select_related(*related_fields).in_bulk(ids_to_fetch)
|
|
70
|
+
|
|
71
|
+
for obj in new_records:
|
|
72
|
+
preloaded = fetched.get(obj.pk)
|
|
73
|
+
if not preloaded:
|
|
74
|
+
continue
|
|
75
|
+
for field in related_fields:
|
|
76
|
+
if field in obj._state.fields_cache:
|
|
77
|
+
# don't override values that were explicitly set or already loaded
|
|
78
|
+
continue
|
|
79
|
+
if "." in field:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"@preload_related does not support nested fields like '{field}'"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
f = model_cls._meta.get_field(field)
|
|
86
|
+
if not (
|
|
87
|
+
f.is_relation and not f.many_to_many and not f.one_to_many
|
|
88
|
+
):
|
|
89
|
+
continue
|
|
90
|
+
except FieldDoesNotExist:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
rel_obj = getattr(preloaded, field)
|
|
95
|
+
setattr(obj, field, rel_obj)
|
|
96
|
+
obj._state.fields_cache[field] = rel_obj
|
|
97
|
+
except AttributeError:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
return func(*bound.args, **bound.kwargs)
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
43
103
|
|
|
44
104
|
return decorator
|
|
45
105
|
|