django-bulk-hooks 0.1.95__tar.gz → 0.1.97__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.
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/PKG-INFO +3 -3
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/__init__.py +60 -52
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/conditions.py +316 -236
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/engine.py +82 -82
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/handler.py +4 -2
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/LICENSE +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/README.md +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.1.95 → django_bulk_hooks-0.1.97}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.97
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
+
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
5
6
|
License: MIT
|
|
6
7
|
Keywords: django,bulk,hooks
|
|
7
8
|
Author: Konrad Beck
|
|
@@ -13,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Requires-Dist: Django (>=4.0)
|
|
16
|
-
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -1,52 +1,60 @@
|
|
|
1
|
-
from django_bulk_hooks.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
1
|
+
from django_bulk_hooks.conditions import (
|
|
2
|
+
ChangesTo,
|
|
3
|
+
HasChanged,
|
|
4
|
+
IsBlank,
|
|
5
|
+
IsEqual,
|
|
6
|
+
IsGreaterThan,
|
|
7
|
+
IsGreaterThanOrEqual,
|
|
8
|
+
IsLessThan,
|
|
9
|
+
IsLessThanOrEqual,
|
|
10
|
+
IsNotEqual,
|
|
11
|
+
WasEqual,
|
|
12
|
+
is_field_set,
|
|
13
|
+
safe_get_related_attr,
|
|
14
|
+
safe_get_related_object,
|
|
15
|
+
)
|
|
16
|
+
from django_bulk_hooks.constants import (
|
|
17
|
+
AFTER_CREATE,
|
|
18
|
+
AFTER_DELETE,
|
|
19
|
+
AFTER_UPDATE,
|
|
20
|
+
BEFORE_CREATE,
|
|
21
|
+
BEFORE_DELETE,
|
|
22
|
+
BEFORE_UPDATE,
|
|
23
|
+
VALIDATE_CREATE,
|
|
24
|
+
VALIDATE_DELETE,
|
|
25
|
+
VALIDATE_UPDATE,
|
|
26
|
+
)
|
|
27
|
+
from django_bulk_hooks.decorators import hook, select_related
|
|
28
|
+
from django_bulk_hooks.enums import Priority
|
|
29
|
+
from django_bulk_hooks.handler import HookHandler
|
|
30
|
+
from django_bulk_hooks.models import HookModelMixin
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"HookHandler",
|
|
34
|
+
"HookModelMixin",
|
|
35
|
+
"BEFORE_CREATE",
|
|
36
|
+
"AFTER_CREATE",
|
|
37
|
+
"BEFORE_UPDATE",
|
|
38
|
+
"AFTER_UPDATE",
|
|
39
|
+
"BEFORE_DELETE",
|
|
40
|
+
"AFTER_DELETE",
|
|
41
|
+
"VALIDATE_CREATE",
|
|
42
|
+
"VALIDATE_UPDATE",
|
|
43
|
+
"VALIDATE_DELETE",
|
|
44
|
+
"safe_get_related_object",
|
|
45
|
+
"safe_get_related_attr",
|
|
46
|
+
"is_field_set",
|
|
47
|
+
"Priority",
|
|
48
|
+
"hook",
|
|
49
|
+
"select_related",
|
|
50
|
+
"ChangesTo",
|
|
51
|
+
"HasChanged",
|
|
52
|
+
"IsEqual",
|
|
53
|
+
"IsNotEqual",
|
|
54
|
+
"WasEqual",
|
|
55
|
+
"IsBlank",
|
|
56
|
+
"IsGreaterThan",
|
|
57
|
+
"IsLessThan",
|
|
58
|
+
"IsGreaterThanOrEqual",
|
|
59
|
+
"IsLessThanOrEqual",
|
|
60
|
+
]
|
|
@@ -1,236 +1,316 @@
|
|
|
1
|
-
from django.db import models
|
|
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,
|
|
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):
|
|
84
|
-
"""
|
|
85
|
-
Check if a field has been set on a model instance.
|
|
86
|
-
|
|
87
|
-
This is useful for checking if a field has been explicitly set,
|
|
88
|
-
even if it's been set to None.
|
|
89
|
-
"""
|
|
90
|
-
return hasattr(instance, field_name)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class HookCondition:
|
|
94
|
-
def check(self, instance, original_instance=None):
|
|
95
|
-
raise NotImplementedError
|
|
96
|
-
|
|
97
|
-
def __call__(self, instance, original_instance=None):
|
|
98
|
-
return self.check(instance, original_instance)
|
|
99
|
-
|
|
100
|
-
def __and__(self, other):
|
|
101
|
-
return AndCondition(self, other)
|
|
102
|
-
|
|
103
|
-
def __or__(self, other):
|
|
104
|
-
return OrCondition(self, other)
|
|
105
|
-
|
|
106
|
-
def __invert__(self):
|
|
107
|
-
return NotCondition(self)
|
|
108
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
self.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
self.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1
|
+
from django.db import models
|
|
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):
|
|
84
|
+
"""
|
|
85
|
+
Check if a field has been set on a model instance.
|
|
86
|
+
|
|
87
|
+
This is useful for checking if a field has been explicitly set,
|
|
88
|
+
even if it's been set to None.
|
|
89
|
+
"""
|
|
90
|
+
return hasattr(instance, field_name)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class HookCondition:
|
|
94
|
+
def check(self, instance, original_instance=None):
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
def __call__(self, instance, original_instance=None):
|
|
98
|
+
return self.check(instance, original_instance)
|
|
99
|
+
|
|
100
|
+
def __and__(self, other):
|
|
101
|
+
return AndCondition(self, other)
|
|
102
|
+
|
|
103
|
+
def __or__(self, other):
|
|
104
|
+
return OrCondition(self, other)
|
|
105
|
+
|
|
106
|
+
def __invert__(self):
|
|
107
|
+
return NotCondition(self)
|
|
108
|
+
|
|
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
|
+
|
|
132
|
+
|
|
133
|
+
class AndCondition(HookCondition):
|
|
134
|
+
def __init__(self, *conditions):
|
|
135
|
+
self.conditions = conditions
|
|
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
|
|
164
|
+
|
|
165
|
+
def check(self, instance, original_instance=None):
|
|
166
|
+
return not self.condition.check(instance, original_instance)
|
|
167
|
+
|
|
168
|
+
def get_required_fields(self):
|
|
169
|
+
return self.condition.get_required_fields()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class IsEqual(HookCondition):
|
|
173
|
+
def __init__(self, field_name, value):
|
|
174
|
+
self.field_name = field_name
|
|
175
|
+
self.value = value
|
|
176
|
+
|
|
177
|
+
def check(self, instance, original_instance=None):
|
|
178
|
+
return getattr(instance, self.field_name, None) == self.value
|
|
179
|
+
|
|
180
|
+
def get_required_fields(self):
|
|
181
|
+
return {self.field_name}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class WasEqual(HookCondition):
|
|
185
|
+
def __init__(self, field_name, value):
|
|
186
|
+
self.field_name = field_name
|
|
187
|
+
self.value = value
|
|
188
|
+
|
|
189
|
+
def check(self, instance, original_instance=None):
|
|
190
|
+
if original_instance is None:
|
|
191
|
+
return False
|
|
192
|
+
return getattr(original_instance, self.field_name, None) == self.value
|
|
193
|
+
|
|
194
|
+
def get_required_fields(self):
|
|
195
|
+
return {self.field_name}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class HasChanged(HookCondition):
|
|
199
|
+
def __init__(self, field_name, has_changed=True):
|
|
200
|
+
"""
|
|
201
|
+
Check if a field's value has changed or remained the same.
|
|
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
|
|
212
|
+
"""
|
|
213
|
+
self.field_name = field_name
|
|
214
|
+
self.has_changed = has_changed
|
|
215
|
+
|
|
216
|
+
def check(self, instance, original_instance=None):
|
|
217
|
+
if original_instance is None:
|
|
218
|
+
# For new instances:
|
|
219
|
+
# - If we're checking for changes (has_changed=True), return True since it's a new record
|
|
220
|
+
# - If we're checking for stability (has_changed=False), return False since it's technically changed from nothing
|
|
221
|
+
return self.has_changed
|
|
222
|
+
|
|
223
|
+
current_value = getattr(instance, self.field_name, None)
|
|
224
|
+
original_value = getattr(original_instance, self.field_name, None)
|
|
225
|
+
return (current_value != original_value) == self.has_changed
|
|
226
|
+
|
|
227
|
+
def get_required_fields(self):
|
|
228
|
+
return {self.field_name}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ChangesTo(HookCondition):
|
|
232
|
+
def __init__(self, field_name, value):
|
|
233
|
+
self.field_name = field_name
|
|
234
|
+
self.value = value
|
|
235
|
+
|
|
236
|
+
def check(self, instance, original_instance=None):
|
|
237
|
+
if original_instance is None:
|
|
238
|
+
return getattr(instance, self.field_name, None) == self.value
|
|
239
|
+
return getattr(instance, self.field_name, None) == self.value and getattr(
|
|
240
|
+
instance, self.field_name, None
|
|
241
|
+
) != getattr(original_instance, self.field_name, None)
|
|
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}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class IsGreaterThan(HookCondition):
|
|
260
|
+
def __init__(self, field_name, value):
|
|
261
|
+
self.field_name = field_name
|
|
262
|
+
self.value = value
|
|
263
|
+
|
|
264
|
+
def check(self, instance, original_instance=None):
|
|
265
|
+
field_value = getattr(instance, self.field_name, None)
|
|
266
|
+
if field_value is None:
|
|
267
|
+
return False
|
|
268
|
+
return field_value > self.value
|
|
269
|
+
|
|
270
|
+
def get_required_fields(self):
|
|
271
|
+
return {self.field_name}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class IsLessThan(HookCondition):
|
|
275
|
+
def __init__(self, field_name, value):
|
|
276
|
+
self.field_name = field_name
|
|
277
|
+
self.value = value
|
|
278
|
+
|
|
279
|
+
def check(self, instance, original_instance=None):
|
|
280
|
+
field_value = getattr(instance, self.field_name, None)
|
|
281
|
+
if field_value is None:
|
|
282
|
+
return False
|
|
283
|
+
return field_value < self.value
|
|
284
|
+
|
|
285
|
+
def get_required_fields(self):
|
|
286
|
+
return {self.field_name}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class IsGreaterThanOrEqual(HookCondition):
|
|
290
|
+
def __init__(self, field_name, value):
|
|
291
|
+
self.field_name = field_name
|
|
292
|
+
self.value = value
|
|
293
|
+
|
|
294
|
+
def check(self, instance, original_instance=None):
|
|
295
|
+
field_value = getattr(instance, self.field_name, None)
|
|
296
|
+
if field_value is None:
|
|
297
|
+
return False
|
|
298
|
+
return field_value >= self.value
|
|
299
|
+
|
|
300
|
+
def get_required_fields(self):
|
|
301
|
+
return {self.field_name}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class IsLessThanOrEqual(HookCondition):
|
|
305
|
+
def __init__(self, field_name, value):
|
|
306
|
+
self.field_name = field_name
|
|
307
|
+
self.value = value
|
|
308
|
+
|
|
309
|
+
def check(self, instance, original_instance=None):
|
|
310
|
+
field_value = getattr(instance, self.field_name, None)
|
|
311
|
+
if field_value is None:
|
|
312
|
+
return False
|
|
313
|
+
return field_value <= self.value
|
|
314
|
+
|
|
315
|
+
def get_required_fields(self):
|
|
316
|
+
return {self.field_name}
|
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from django.core.exceptions import ValidationError
|
|
4
|
-
from django.db import models
|
|
5
|
-
from django_bulk_hooks.registry import get_hooks
|
|
6
|
-
from django_bulk_hooks.conditions import safe_get_related_object, safe_get_related_attr
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Cache for hook handlers to avoid creating them repeatedly
|
|
12
|
-
_handler_cache = {}
|
|
13
|
-
|
|
14
|
-
def run(model_cls, event, new_instances, original_instances=None, ctx=None):
|
|
15
|
-
# Get hooks from cache or fetch them
|
|
16
|
-
cache_key = (model_cls, event)
|
|
17
|
-
hooks = get_hooks(model_cls, event)
|
|
18
|
-
|
|
19
|
-
if not hooks:
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
# For BEFORE_* events, run model.clean() first for validation
|
|
23
|
-
if event.startswith("before_"):
|
|
24
|
-
for instance in new_instances:
|
|
25
|
-
try:
|
|
26
|
-
instance.clean()
|
|
27
|
-
except ValidationError as e:
|
|
28
|
-
logger.error("Validation failed for %s: %s", instance, e)
|
|
29
|
-
raise
|
|
30
|
-
except Exception as e:
|
|
31
|
-
# Handle RelatedObjectDoesNotExist and other exceptions that might occur
|
|
32
|
-
# when accessing foreign key fields on unsaved objects
|
|
33
|
-
if "RelatedObjectDoesNotExist" in str(type(e).__name__):
|
|
34
|
-
logger.debug("Skipping validation for unsaved object with unset foreign keys: %s", e)
|
|
35
|
-
continue
|
|
36
|
-
else:
|
|
37
|
-
logger.error("Unexpected error during validation for %s: %s", instance, e)
|
|
38
|
-
raise
|
|
39
|
-
|
|
40
|
-
# Pre-create None list for originals if needed
|
|
41
|
-
if original_instances is None:
|
|
42
|
-
original_instances = [None] * len(new_instances)
|
|
43
|
-
|
|
44
|
-
# Process all hooks
|
|
45
|
-
for handler_cls, method_name, condition, priority in hooks:
|
|
46
|
-
# Get or create handler instance from cache
|
|
47
|
-
handler_key = (handler_cls, method_name)
|
|
48
|
-
if handler_key not in _handler_cache:
|
|
49
|
-
handler_instance = handler_cls()
|
|
50
|
-
func = getattr(handler_instance, method_name)
|
|
51
|
-
_handler_cache[handler_key] = (handler_instance, func)
|
|
52
|
-
else:
|
|
53
|
-
handler_instance, func = _handler_cache[handler_key]
|
|
54
|
-
|
|
55
|
-
# Filter instances based on condition
|
|
56
|
-
if condition:
|
|
57
|
-
to_process_new = []
|
|
58
|
-
to_process_old = []
|
|
59
|
-
|
|
60
|
-
logger.debug(f"Checking condition {condition.__class__.__name__} for {len(new_instances)} instances")
|
|
61
|
-
for new, original in zip(new_instances, original_instances, strict=True):
|
|
62
|
-
logger.debug(f"Checking instance {new.__class__.__name__}(pk={new.pk})")
|
|
63
|
-
try:
|
|
64
|
-
matches = condition.check(new, original)
|
|
65
|
-
logger.debug(f"Condition check result: {matches}")
|
|
66
|
-
if matches:
|
|
67
|
-
to_process_new.append(new)
|
|
68
|
-
to_process_old.append(original)
|
|
69
|
-
except Exception as e:
|
|
70
|
-
logger.error(f"Error checking condition: {e}")
|
|
71
|
-
raise
|
|
72
|
-
|
|
73
|
-
# Only call if we have matching instances
|
|
74
|
-
if to_process_new:
|
|
75
|
-
logger.debug(f"Running hook for {len(to_process_new)} matching instances")
|
|
76
|
-
func(new_records=to_process_new, old_records=to_process_old if any(to_process_old) else None)
|
|
77
|
-
else:
|
|
78
|
-
logger.debug("No instances matched condition")
|
|
79
|
-
else:
|
|
80
|
-
# No condition, process all instances
|
|
81
|
-
logger.debug("No condition, processing all instances")
|
|
82
|
-
func(new_records=new_instances, old_records=original_instances if any(original_instances) else None)
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import ValidationError
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django_bulk_hooks.registry import get_hooks
|
|
6
|
+
from django_bulk_hooks.conditions import safe_get_related_object, safe_get_related_attr
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Cache for hook handlers to avoid creating them repeatedly
|
|
12
|
+
_handler_cache = {}
|
|
13
|
+
|
|
14
|
+
def run(model_cls, event, new_instances, original_instances=None, ctx=None):
|
|
15
|
+
# Get hooks from cache or fetch them
|
|
16
|
+
cache_key = (model_cls, event)
|
|
17
|
+
hooks = get_hooks(model_cls, event)
|
|
18
|
+
|
|
19
|
+
if not hooks:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# For BEFORE_* events, run model.clean() first for validation
|
|
23
|
+
if event.startswith("before_"):
|
|
24
|
+
for instance in new_instances:
|
|
25
|
+
try:
|
|
26
|
+
instance.clean()
|
|
27
|
+
except ValidationError as e:
|
|
28
|
+
logger.error("Validation failed for %s: %s", instance, e)
|
|
29
|
+
raise
|
|
30
|
+
except Exception as e:
|
|
31
|
+
# Handle RelatedObjectDoesNotExist and other exceptions that might occur
|
|
32
|
+
# when accessing foreign key fields on unsaved objects
|
|
33
|
+
if "RelatedObjectDoesNotExist" in str(type(e).__name__):
|
|
34
|
+
logger.debug("Skipping validation for unsaved object with unset foreign keys: %s", e)
|
|
35
|
+
continue
|
|
36
|
+
else:
|
|
37
|
+
logger.error("Unexpected error during validation for %s: %s", instance, e)
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
# Pre-create None list for originals if needed
|
|
41
|
+
if original_instances is None:
|
|
42
|
+
original_instances = [None] * len(new_instances)
|
|
43
|
+
|
|
44
|
+
# Process all hooks
|
|
45
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
46
|
+
# Get or create handler instance from cache
|
|
47
|
+
handler_key = (handler_cls, method_name)
|
|
48
|
+
if handler_key not in _handler_cache:
|
|
49
|
+
handler_instance = handler_cls()
|
|
50
|
+
func = getattr(handler_instance, method_name)
|
|
51
|
+
_handler_cache[handler_key] = (handler_instance, func)
|
|
52
|
+
else:
|
|
53
|
+
handler_instance, func = _handler_cache[handler_key]
|
|
54
|
+
|
|
55
|
+
# Filter instances based on condition
|
|
56
|
+
if condition:
|
|
57
|
+
to_process_new = []
|
|
58
|
+
to_process_old = []
|
|
59
|
+
|
|
60
|
+
logger.debug(f"Checking condition {condition.__class__.__name__} for {len(new_instances)} instances")
|
|
61
|
+
for new, original in zip(new_instances, original_instances, strict=True):
|
|
62
|
+
logger.debug(f"Checking instance {new.__class__.__name__}(pk={new.pk})")
|
|
63
|
+
try:
|
|
64
|
+
matches = condition.check(new, original)
|
|
65
|
+
logger.debug(f"Condition check result: {matches}")
|
|
66
|
+
if matches:
|
|
67
|
+
to_process_new.append(new)
|
|
68
|
+
to_process_old.append(original)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Error checking condition: {e}")
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
# Only call if we have matching instances
|
|
74
|
+
if to_process_new:
|
|
75
|
+
logger.debug(f"Running hook for {len(to_process_new)} matching instances")
|
|
76
|
+
func(new_records=to_process_new, old_records=to_process_old if any(to_process_old) else None)
|
|
77
|
+
else:
|
|
78
|
+
logger.debug("No instances matched condition")
|
|
79
|
+
else:
|
|
80
|
+
# No condition, process all instances
|
|
81
|
+
logger.debug("No condition, processing all instances")
|
|
82
|
+
func(new_records=new_instances, old_records=original_instances if any(original_instances) else None)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
2
3
|
import threading
|
|
3
4
|
from collections import deque
|
|
@@ -144,11 +145,12 @@ class HookHandler(metaclass=HookMeta):
|
|
|
144
145
|
|
|
145
146
|
# Inspect the method signature to determine parameter order
|
|
146
147
|
import inspect
|
|
148
|
+
|
|
147
149
|
sig = inspect.signature(method)
|
|
148
150
|
params = list(sig.parameters.keys())
|
|
149
|
-
|
|
151
|
+
|
|
150
152
|
# Remove 'self' from params if it exists
|
|
151
|
-
if params and params[0] ==
|
|
153
|
+
if params and params[0] == "self":
|
|
152
154
|
params = params[1:]
|
|
153
155
|
|
|
154
156
|
# Always call with keyword arguments to make order irrelevant
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|