django-bulk-hooks 0.1.93__tar.gz → 0.1.94__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.93 → django_bulk_hooks-0.1.94}/PKG-INFO +1 -1
- django_bulk_hooks-0.1.94/django_bulk_hooks/conditions.py +236 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.93/django_bulk_hooks/conditions.py +0 -381
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/LICENSE +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/README.md +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.1.93 → django_bulk_hooks-0.1.94}/django_bulk_hooks/registry.py +0 -0
|
@@ -0,0 +1,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
|
+
def __init__(self, field_name):
|
|
122
|
+
self.field_name = field_name
|
|
123
|
+
|
|
124
|
+
def check(self, instance, original_instance=None):
|
|
125
|
+
value = getattr(instance, self.field_name, None)
|
|
126
|
+
return value is None or value == ""
|
|
127
|
+
|
|
128
|
+
def get_required_fields(self):
|
|
129
|
+
return {self.field_name}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AndCondition(HookCondition):
|
|
133
|
+
def __init__(self, *conditions):
|
|
134
|
+
self.conditions = conditions
|
|
135
|
+
|
|
136
|
+
def check(self, instance, original_instance=None):
|
|
137
|
+
return all(c.check(instance, original_instance) for c in self.conditions)
|
|
138
|
+
|
|
139
|
+
def get_required_fields(self):
|
|
140
|
+
fields = set()
|
|
141
|
+
for condition in self.conditions:
|
|
142
|
+
fields.update(condition.get_required_fields())
|
|
143
|
+
return fields
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class OrCondition(HookCondition):
|
|
147
|
+
def __init__(self, *conditions):
|
|
148
|
+
self.conditions = conditions
|
|
149
|
+
|
|
150
|
+
def check(self, instance, original_instance=None):
|
|
151
|
+
return any(c.check(instance, original_instance) for c in self.conditions)
|
|
152
|
+
|
|
153
|
+
def get_required_fields(self):
|
|
154
|
+
fields = set()
|
|
155
|
+
for condition in self.conditions:
|
|
156
|
+
fields.update(condition.get_required_fields())
|
|
157
|
+
return fields
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class NotCondition(HookCondition):
|
|
161
|
+
def __init__(self, condition):
|
|
162
|
+
self.condition = condition
|
|
163
|
+
|
|
164
|
+
def check(self, instance, original_instance=None):
|
|
165
|
+
return not self.condition.check(instance, original_instance)
|
|
166
|
+
|
|
167
|
+
def get_required_fields(self):
|
|
168
|
+
return self.condition.get_required_fields()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class IsEqual(HookCondition):
|
|
172
|
+
def __init__(self, field_name, value):
|
|
173
|
+
self.field_name = field_name
|
|
174
|
+
self.value = value
|
|
175
|
+
|
|
176
|
+
def check(self, instance, original_instance=None):
|
|
177
|
+
return getattr(instance, self.field_name, None) == self.value
|
|
178
|
+
|
|
179
|
+
def get_required_fields(self):
|
|
180
|
+
return {self.field_name}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class WasEqual(HookCondition):
|
|
184
|
+
def __init__(self, field_name, value):
|
|
185
|
+
self.field_name = field_name
|
|
186
|
+
self.value = value
|
|
187
|
+
|
|
188
|
+
def check(self, instance, original_instance=None):
|
|
189
|
+
if original_instance is None:
|
|
190
|
+
return False
|
|
191
|
+
return getattr(original_instance, self.field_name, None) == self.value
|
|
192
|
+
|
|
193
|
+
def get_required_fields(self):
|
|
194
|
+
return {self.field_name}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class HasChanged(HookCondition):
|
|
198
|
+
def __init__(self, field_name):
|
|
199
|
+
self.field_name = field_name
|
|
200
|
+
|
|
201
|
+
def check(self, instance, original_instance=None):
|
|
202
|
+
if original_instance is None:
|
|
203
|
+
return True
|
|
204
|
+
return getattr(instance, self.field_name, None) != getattr(original_instance, self.field_name, None)
|
|
205
|
+
|
|
206
|
+
def get_required_fields(self):
|
|
207
|
+
return {self.field_name}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ChangesTo(HookCondition):
|
|
211
|
+
def __init__(self, field_name, value):
|
|
212
|
+
self.field_name = field_name
|
|
213
|
+
self.value = value
|
|
214
|
+
|
|
215
|
+
def check(self, instance, original_instance=None):
|
|
216
|
+
if original_instance is None:
|
|
217
|
+
return getattr(instance, self.field_name, None) == self.value
|
|
218
|
+
return (
|
|
219
|
+
getattr(instance, self.field_name, None) == self.value
|
|
220
|
+
and getattr(instance, self.field_name, None) != getattr(original_instance, self.field_name, None)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def get_required_fields(self):
|
|
224
|
+
return {self.field_name}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class IsNotEqual(HookCondition):
|
|
228
|
+
def __init__(self, field_name, value):
|
|
229
|
+
self.field_name = field_name
|
|
230
|
+
self.value = value
|
|
231
|
+
|
|
232
|
+
def check(self, instance, original_instance=None):
|
|
233
|
+
return getattr(instance, self.field_name, None) != self.value
|
|
234
|
+
|
|
235
|
+
def get_required_fields(self):
|
|
236
|
+
return {self.field_name}
|
|
@@ -1,381 +0,0 @@
|
|
|
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 is_field_set(instance, field_name):
|
|
33
|
-
"""
|
|
34
|
-
Check if a foreign key field is set without raising RelatedObjectDoesNotExist.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
instance: The model instance
|
|
38
|
-
field_name: The foreign key field name
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
True if the field is set, False otherwise
|
|
42
|
-
"""
|
|
43
|
-
# Check the foreign key ID field first
|
|
44
|
-
fk_field_name = f"{field_name}_id"
|
|
45
|
-
if hasattr(instance, fk_field_name):
|
|
46
|
-
fk_value = getattr(instance, fk_field_name, None)
|
|
47
|
-
return fk_value is not None
|
|
48
|
-
|
|
49
|
-
# Fallback to checking the field directly
|
|
50
|
-
try:
|
|
51
|
-
return getattr(instance, field_name) is not None
|
|
52
|
-
except Exception:
|
|
53
|
-
return False
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def safe_get_related_attr(instance, field_name, attr_name=None):
|
|
57
|
-
"""
|
|
58
|
-
Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
|
|
59
|
-
|
|
60
|
-
This is particularly useful in hooks where objects might not have their related
|
|
61
|
-
fields populated yet (e.g., during bulk_create operations or on unsaved objects).
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
instance: The model instance
|
|
65
|
-
field_name: The foreign key field name
|
|
66
|
-
attr_name: Optional attribute name to access on the related object
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
The related object, the attribute value, or None if not available
|
|
70
|
-
|
|
71
|
-
Example:
|
|
72
|
-
# Instead of: loan_transaction.status.name (which might fail)
|
|
73
|
-
# Use: safe_get_related_attr(loan_transaction, 'status', 'name')
|
|
74
|
-
|
|
75
|
-
status_name = safe_get_related_attr(loan_transaction, 'status', 'name')
|
|
76
|
-
if status_name in {Status.COMPLETE.value, Status.FAILED.value}:
|
|
77
|
-
# Process the transaction
|
|
78
|
-
pass
|
|
79
|
-
"""
|
|
80
|
-
# For unsaved objects, check the foreign key ID field first
|
|
81
|
-
if instance.pk is None:
|
|
82
|
-
fk_field_name = f"{field_name}_id"
|
|
83
|
-
if hasattr(instance, fk_field_name):
|
|
84
|
-
fk_value = getattr(instance, fk_field_name, None)
|
|
85
|
-
if fk_value is None:
|
|
86
|
-
return None
|
|
87
|
-
# If we have an ID but the object isn't loaded, try to load it
|
|
88
|
-
try:
|
|
89
|
-
field = instance._meta.get_field(field_name)
|
|
90
|
-
if hasattr(field, 'related_model'):
|
|
91
|
-
related_obj = field.related_model.objects.get(id=fk_value)
|
|
92
|
-
if attr_name is None:
|
|
93
|
-
return related_obj
|
|
94
|
-
return getattr(related_obj, attr_name, None)
|
|
95
|
-
except (field.related_model.DoesNotExist, AttributeError):
|
|
96
|
-
return None
|
|
97
|
-
|
|
98
|
-
# For saved objects or when the above doesn't work, use the original method
|
|
99
|
-
related_obj = safe_get_related_object(instance, field_name)
|
|
100
|
-
if related_obj is None:
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
if attr_name is None:
|
|
104
|
-
return related_obj
|
|
105
|
-
|
|
106
|
-
return getattr(related_obj, attr_name, None)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def safe_get_related_attr_with_fallback(instance, field_name, attr_name=None, fallback_value=None):
|
|
110
|
-
"""
|
|
111
|
-
Enhanced version of safe_get_related_attr that provides fallback handling.
|
|
112
|
-
|
|
113
|
-
This function is especially useful for bulk operations where related objects
|
|
114
|
-
might not be fully loaded or might not exist yet.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
instance: The model instance
|
|
118
|
-
field_name: The foreign key field name
|
|
119
|
-
attr_name: Optional attribute name to access on the related object
|
|
120
|
-
fallback_value: Value to return if the related object or attribute doesn't exist
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
The related object, the attribute value, or fallback_value if not available
|
|
124
|
-
"""
|
|
125
|
-
# First try the standard safe access
|
|
126
|
-
result = safe_get_related_attr(instance, field_name, attr_name)
|
|
127
|
-
if result is not None:
|
|
128
|
-
return result
|
|
129
|
-
|
|
130
|
-
# If that fails, try to get the foreign key ID and fetch the object directly
|
|
131
|
-
fk_field_name = f"{field_name}_id"
|
|
132
|
-
if hasattr(instance, fk_field_name):
|
|
133
|
-
fk_id = getattr(instance, fk_field_name)
|
|
134
|
-
if fk_id is not None:
|
|
135
|
-
try:
|
|
136
|
-
# Get the field to determine the related model
|
|
137
|
-
field = instance._meta.get_field(field_name)
|
|
138
|
-
if field.is_relation and not field.many_to_many and not field.one_to_many:
|
|
139
|
-
# Try to fetch the related object directly
|
|
140
|
-
related_obj = field.related_model.objects.get(pk=fk_id)
|
|
141
|
-
if attr_name is None:
|
|
142
|
-
return related_obj
|
|
143
|
-
return getattr(related_obj, attr_name, fallback_value)
|
|
144
|
-
except (field.related_model.DoesNotExist, AttributeError):
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
|
-
return fallback_value
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def resolve_dotted_attr(instance, dotted_path):
|
|
151
|
-
"""
|
|
152
|
-
Recursively resolve a dotted attribute path, e.g., "type.category".
|
|
153
|
-
This function is designed to work with pre-loaded foreign keys to avoid queries.
|
|
154
|
-
"""
|
|
155
|
-
if instance is None:
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
current = instance
|
|
159
|
-
for attr in dotted_path.split("."):
|
|
160
|
-
if current is None:
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
# Check if this is a foreign key that might trigger a query
|
|
164
|
-
if hasattr(current, '_meta') and hasattr(current._meta, 'get_field'):
|
|
165
|
-
try:
|
|
166
|
-
field = current._meta.get_field(attr)
|
|
167
|
-
if field.is_relation and not field.many_to_many and not field.one_to_many:
|
|
168
|
-
# For foreign keys, use safe access to prevent RelatedObjectDoesNotExist
|
|
169
|
-
current = safe_get_related_object(current, attr)
|
|
170
|
-
else:
|
|
171
|
-
current = getattr(current, attr, None)
|
|
172
|
-
except Exception:
|
|
173
|
-
# If field lookup fails, fall back to regular attribute access
|
|
174
|
-
current = getattr(current, attr, None)
|
|
175
|
-
else:
|
|
176
|
-
# Not a model instance, use regular attribute access
|
|
177
|
-
current = getattr(current, attr, None)
|
|
178
|
-
|
|
179
|
-
return current
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class HookCondition:
|
|
183
|
-
def check(self, instance, original_instance=None):
|
|
184
|
-
raise NotImplementedError
|
|
185
|
-
|
|
186
|
-
def __call__(self, instance, original_instance=None):
|
|
187
|
-
return self.check(instance, original_instance)
|
|
188
|
-
|
|
189
|
-
def __and__(self, other):
|
|
190
|
-
return AndCondition(self, other)
|
|
191
|
-
|
|
192
|
-
def __or__(self, other):
|
|
193
|
-
return OrCondition(self, other)
|
|
194
|
-
|
|
195
|
-
def __invert__(self):
|
|
196
|
-
return NotCondition(self)
|
|
197
|
-
|
|
198
|
-
def get_required_fields(self):
|
|
199
|
-
"""
|
|
200
|
-
Returns a set of field names that this condition needs to evaluate.
|
|
201
|
-
Override in subclasses to specify required fields.
|
|
202
|
-
"""
|
|
203
|
-
return set()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
class IsEqual(HookCondition):
|
|
207
|
-
def __init__(self, field, value):
|
|
208
|
-
self.field = field
|
|
209
|
-
self.value = value
|
|
210
|
-
|
|
211
|
-
def check(self, instance, original_instance=None):
|
|
212
|
-
# Handle the case where the field might not exist yet
|
|
213
|
-
try:
|
|
214
|
-
current_value = getattr(instance, self.field, None)
|
|
215
|
-
return current_value == self.value
|
|
216
|
-
except Exception:
|
|
217
|
-
# If there's any error accessing the field, treat it as None
|
|
218
|
-
return self.value is None
|
|
219
|
-
|
|
220
|
-
def get_required_fields(self):
|
|
221
|
-
return {self.field.split('.')[0]}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
class IsNotEqual(HookCondition):
|
|
225
|
-
def __init__(self, field, value):
|
|
226
|
-
self.field = field
|
|
227
|
-
self.value = value
|
|
228
|
-
|
|
229
|
-
def check(self, instance, original_instance=None):
|
|
230
|
-
current_value = resolve_dotted_attr(instance, self.field)
|
|
231
|
-
return current_value != self.value
|
|
232
|
-
|
|
233
|
-
def get_required_fields(self):
|
|
234
|
-
return {self.field.split('.')[0]}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class WasEqual(HookCondition):
|
|
238
|
-
def __init__(self, field, value):
|
|
239
|
-
self.field = field
|
|
240
|
-
self.value = value
|
|
241
|
-
|
|
242
|
-
def check(self, instance, original_instance=None):
|
|
243
|
-
if original_instance is None:
|
|
244
|
-
return False
|
|
245
|
-
original_value = resolve_dotted_attr(original_instance, self.field)
|
|
246
|
-
return original_value == self.value
|
|
247
|
-
|
|
248
|
-
def get_required_fields(self):
|
|
249
|
-
return {self.field.split('.')[0]}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
class HasChanged(HookCondition):
|
|
253
|
-
def __init__(self, field, has_changed=True):
|
|
254
|
-
"""
|
|
255
|
-
Check if a field's value has changed or remained the same.
|
|
256
|
-
|
|
257
|
-
Args:
|
|
258
|
-
field: The field name to check
|
|
259
|
-
has_changed: If True (default), condition passes when field has changed.
|
|
260
|
-
If False, condition passes when field has remained the same.
|
|
261
|
-
This is useful for:
|
|
262
|
-
- Detecting stable/unchanged fields
|
|
263
|
-
- Validating field immutability
|
|
264
|
-
- Ensuring critical fields remain constant
|
|
265
|
-
- State machine validations
|
|
266
|
-
"""
|
|
267
|
-
self.field = field
|
|
268
|
-
self.has_changed = has_changed
|
|
269
|
-
|
|
270
|
-
def check(self, instance, original_instance=None):
|
|
271
|
-
if original_instance is None:
|
|
272
|
-
# For new instances:
|
|
273
|
-
# - If we're checking for changes (has_changed=True), return False since there's no change yet
|
|
274
|
-
# - If we're checking for stability (has_changed=False), return True since it's technically unchanged
|
|
275
|
-
return not self.has_changed
|
|
276
|
-
|
|
277
|
-
current_value = resolve_dotted_attr(instance, self.field)
|
|
278
|
-
original_value = resolve_dotted_attr(original_instance, self.field)
|
|
279
|
-
return (current_value != original_value) == self.has_changed
|
|
280
|
-
|
|
281
|
-
def get_required_fields(self):
|
|
282
|
-
return {self.field.split('.')[0]}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
class ChangesTo(HookCondition):
|
|
286
|
-
def __init__(self, field, value):
|
|
287
|
-
self.field = field
|
|
288
|
-
self.value = value
|
|
289
|
-
|
|
290
|
-
def check(self, instance, original_instance=None):
|
|
291
|
-
if original_instance is None:
|
|
292
|
-
current_value = resolve_dotted_attr(instance, self.field)
|
|
293
|
-
return current_value == self.value
|
|
294
|
-
|
|
295
|
-
current_value = resolve_dotted_attr(instance, self.field)
|
|
296
|
-
original_value = resolve_dotted_attr(original_instance, self.field)
|
|
297
|
-
return current_value == self.value and current_value != original_value
|
|
298
|
-
|
|
299
|
-
def get_required_fields(self):
|
|
300
|
-
return {self.field.split('.')[0]}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
class IsGreaterThan(HookCondition):
|
|
304
|
-
def __init__(self, field, value):
|
|
305
|
-
self.field = field
|
|
306
|
-
self.value = value
|
|
307
|
-
|
|
308
|
-
def check(self, instance, original_instance=None):
|
|
309
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
310
|
-
return current is not None and current > self.value
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
class IsGreaterThanOrEqual(HookCondition):
|
|
314
|
-
def __init__(self, field, value):
|
|
315
|
-
self.field = field
|
|
316
|
-
self.value = value
|
|
317
|
-
|
|
318
|
-
def check(self, instance, original_instance=None):
|
|
319
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
320
|
-
return current is not None and current >= self.value
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
class IsLessThan(HookCondition):
|
|
324
|
-
def __init__(self, field, value):
|
|
325
|
-
self.field = field
|
|
326
|
-
self.value = value
|
|
327
|
-
|
|
328
|
-
def check(self, instance, original_instance=None):
|
|
329
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
330
|
-
return current is not None and current < self.value
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
class IsLessThanOrEqual(HookCondition):
|
|
334
|
-
def __init__(self, field, value):
|
|
335
|
-
self.field = field
|
|
336
|
-
self.value = value
|
|
337
|
-
|
|
338
|
-
def check(self, instance, original_instance=None):
|
|
339
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
340
|
-
return current is not None and current <= self.value
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
class AndCondition(HookCondition):
|
|
344
|
-
def __init__(self, condition1, condition2):
|
|
345
|
-
self.condition1 = condition1
|
|
346
|
-
self.condition2 = condition2
|
|
347
|
-
|
|
348
|
-
def check(self, instance, original_instance=None):
|
|
349
|
-
return (
|
|
350
|
-
self.condition1.check(instance, original_instance)
|
|
351
|
-
and self.condition2.check(instance, original_instance)
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
def get_required_fields(self):
|
|
355
|
-
return self.condition1.get_required_fields() | self.condition2.get_required_fields()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
class OrCondition(HookCondition):
|
|
359
|
-
def __init__(self, condition1, condition2):
|
|
360
|
-
self.condition1 = condition1
|
|
361
|
-
self.condition2 = condition2
|
|
362
|
-
|
|
363
|
-
def check(self, instance, original_instance=None):
|
|
364
|
-
return (
|
|
365
|
-
self.condition1.check(instance, original_instance)
|
|
366
|
-
or self.condition2.check(instance, original_instance)
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
def get_required_fields(self):
|
|
370
|
-
return self.condition1.get_required_fields() | self.condition2.get_required_fields()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
class NotCondition(HookCondition):
|
|
374
|
-
def __init__(self, condition):
|
|
375
|
-
self.condition = condition
|
|
376
|
-
|
|
377
|
-
def check(self, instance, original_instance=None):
|
|
378
|
-
return not self.condition.check(instance, original_instance)
|
|
379
|
-
|
|
380
|
-
def get_required_fields(self):
|
|
381
|
-
return self.condition.get_required_fields()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|