django-bulk-hooks 0.1.97__tar.gz → 0.1.100__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.97 → django_bulk_hooks-0.1.100}/PKG-INFO +164 -1
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/README.md +163 -1
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/__init__.py +2 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/conditions.py +42 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/models.py +31 -15
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/LICENSE +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.1.97 → django_bulk_hooks-0.1.100}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.100
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
5
|
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
6
6
|
License: MIT
|
|
@@ -226,3 +226,166 @@ class EfficientTransactionHandler:
|
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
This approach ensures your hooks are robust and won't fail due to missing related objects, while also being efficient with database queries.
|
|
229
|
+
|
|
230
|
+
## 🎯 Lambda Conditions and Anonymous Functions
|
|
231
|
+
|
|
232
|
+
`django-bulk-hooks` supports using anonymous functions (lambda functions) and custom callables as conditions, giving you maximum flexibility for complex filtering logic.
|
|
233
|
+
|
|
234
|
+
### Using LambdaCondition
|
|
235
|
+
|
|
236
|
+
The `LambdaCondition` class allows you to use lambda functions or any callable as a condition:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from django_bulk_hooks import LambdaCondition
|
|
240
|
+
|
|
241
|
+
class ProductHandler:
|
|
242
|
+
# Simple lambda condition
|
|
243
|
+
@hook(Product, "after_create", condition=LambdaCondition(
|
|
244
|
+
lambda instance: instance.price > 100
|
|
245
|
+
))
|
|
246
|
+
def handle_expensive_products(self, new_records, old_records):
|
|
247
|
+
"""Handle products with price > 100"""
|
|
248
|
+
for product in new_records:
|
|
249
|
+
print(f"Expensive product: {product.name}")
|
|
250
|
+
|
|
251
|
+
# Lambda with multiple conditions
|
|
252
|
+
@hook(Product, "after_update", condition=LambdaCondition(
|
|
253
|
+
lambda instance: instance.price > 50 and instance.is_active and instance.stock_quantity > 0
|
|
254
|
+
))
|
|
255
|
+
def handle_available_expensive_products(self, new_records, old_records):
|
|
256
|
+
"""Handle active products with price > 50 and stock > 0"""
|
|
257
|
+
for product in new_records:
|
|
258
|
+
print(f"Available expensive product: {product.name}")
|
|
259
|
+
|
|
260
|
+
# Lambda comparing with original instance
|
|
261
|
+
@hook(Product, "after_update", condition=LambdaCondition(
|
|
262
|
+
lambda instance, original: original and instance.price > original.price * 1.5
|
|
263
|
+
))
|
|
264
|
+
def handle_significant_price_increases(self, new_records, old_records):
|
|
265
|
+
"""Handle products with >50% price increase"""
|
|
266
|
+
for new_product, old_product in zip(new_records, old_records):
|
|
267
|
+
if old_product:
|
|
268
|
+
increase = ((new_product.price - old_product.price) / old_product.price) * 100
|
|
269
|
+
print(f"Significant price increase: {new_product.name} +{increase:.1f}%")
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Combining Lambda Conditions with Built-in Conditions
|
|
273
|
+
|
|
274
|
+
You can combine lambda conditions with built-in conditions using the `&` (AND) and `|` (OR) operators:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from django_bulk_hooks.conditions import HasChanged, IsEqual
|
|
278
|
+
|
|
279
|
+
class AdvancedProductHandler:
|
|
280
|
+
# Combine lambda with built-in conditions
|
|
281
|
+
@hook(Product, "after_update", condition=(
|
|
282
|
+
HasChanged("price") &
|
|
283
|
+
LambdaCondition(lambda instance: instance.price > 100)
|
|
284
|
+
))
|
|
285
|
+
def handle_expensive_price_changes(self, new_records, old_records):
|
|
286
|
+
"""Handle when expensive products have price changes"""
|
|
287
|
+
for new_product, old_product in zip(new_records, old_records):
|
|
288
|
+
print(f"Expensive product price changed: {new_product.name}")
|
|
289
|
+
|
|
290
|
+
# Complex combined conditions
|
|
291
|
+
@hook(Order, "after_update", condition=(
|
|
292
|
+
LambdaCondition(lambda instance: instance.status == 'completed') &
|
|
293
|
+
LambdaCondition(lambda instance, original: original and instance.total_amount > original.total_amount)
|
|
294
|
+
))
|
|
295
|
+
def handle_completed_orders_with_increased_amount(self, new_records, old_records):
|
|
296
|
+
"""Handle completed orders that had amount increases"""
|
|
297
|
+
for new_order, old_order in zip(new_records, old_records):
|
|
298
|
+
if old_order:
|
|
299
|
+
increase = new_order.total_amount - old_order.total_amount
|
|
300
|
+
print(f"Completed order with amount increase: {new_order.customer_name} +${increase}")
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Custom Condition Classes
|
|
304
|
+
|
|
305
|
+
For reusable logic, you can create custom condition classes:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from django_bulk_hooks.conditions import HookCondition
|
|
309
|
+
|
|
310
|
+
class IsPremiumProduct(HookCondition):
|
|
311
|
+
def check(self, instance, original_instance=None):
|
|
312
|
+
return (
|
|
313
|
+
instance.price > 200 and
|
|
314
|
+
instance.rating >= 4.0 and
|
|
315
|
+
instance.is_active
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def get_required_fields(self):
|
|
319
|
+
return {'price', 'rating', 'is_active'}
|
|
320
|
+
|
|
321
|
+
class ProductHandler:
|
|
322
|
+
@hook(Product, "after_create", condition=IsPremiumProduct())
|
|
323
|
+
def handle_premium_products(self, new_records, old_records):
|
|
324
|
+
"""Handle premium products"""
|
|
325
|
+
for product in new_records:
|
|
326
|
+
print(f"Premium product: {product.name}")
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Lambda Conditions with Required Fields
|
|
330
|
+
|
|
331
|
+
For optimization, you can specify which fields your lambda condition depends on:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
class OptimizedProductHandler:
|
|
335
|
+
@hook(Product, "after_update", condition=LambdaCondition(
|
|
336
|
+
lambda instance: instance.price > 100 and instance.category == 'electronics',
|
|
337
|
+
required_fields={'price', 'category'}
|
|
338
|
+
))
|
|
339
|
+
def handle_expensive_electronics(self, new_records, old_records):
|
|
340
|
+
"""Handle expensive electronics products"""
|
|
341
|
+
for product in new_records:
|
|
342
|
+
print(f"Expensive electronics: {product.name}")
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Best Practices for Lambda Conditions
|
|
346
|
+
|
|
347
|
+
1. **Keep lambdas simple** - Complex logic should be moved to custom condition classes
|
|
348
|
+
2. **Handle None values** - Always check for None before performing operations
|
|
349
|
+
3. **Specify required fields** - This helps with query optimization
|
|
350
|
+
4. **Use descriptive names** - Make your lambda conditions self-documenting
|
|
351
|
+
5. **Test thoroughly** - Lambda conditions can be harder to debug than named functions
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
# ✅ GOOD: Simple, clear lambda
|
|
355
|
+
condition = LambdaCondition(lambda instance: instance.price > 100)
|
|
356
|
+
|
|
357
|
+
# ✅ GOOD: Handles None values
|
|
358
|
+
condition = LambdaCondition(
|
|
359
|
+
lambda instance: instance.price is not None and instance.price > 100
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# ❌ AVOID: Complex logic in lambda
|
|
363
|
+
condition = LambdaCondition(
|
|
364
|
+
lambda instance: (
|
|
365
|
+
instance.price > 100 and
|
|
366
|
+
instance.category in ['electronics', 'computers'] and
|
|
367
|
+
instance.stock_quantity > 0 and
|
|
368
|
+
instance.rating >= 4.0 and
|
|
369
|
+
instance.is_active and
|
|
370
|
+
instance.created_at > datetime.now() - timedelta(days=30)
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# ✅ BETTER: Use custom condition class for complex logic
|
|
375
|
+
class IsRecentExpensiveElectronics(HookCondition):
|
|
376
|
+
def check(self, instance, original_instance=None):
|
|
377
|
+
return (
|
|
378
|
+
instance.price > 100 and
|
|
379
|
+
instance.category in ['electronics', 'computers'] and
|
|
380
|
+
instance.stock_quantity > 0 and
|
|
381
|
+
instance.rating >= 4.0 and
|
|
382
|
+
instance.is_active and
|
|
383
|
+
instance.created_at > datetime.now() - timedelta(days=30)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def get_required_fields(self):
|
|
387
|
+
return {'price', 'category', 'stock_quantity', 'rating', 'is_active', 'created_at'}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## 🔧 Best Practices for Related Objects
|
|
391
|
+
|
|
@@ -206,4 +206,166 @@ class EfficientTransactionHandler:
|
|
|
206
206
|
self._batch_process_failed(transactions)
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
This approach ensures your hooks are robust and won't fail due to missing related objects, while also being efficient with database queries.
|
|
209
|
+
This approach ensures your hooks are robust and won't fail due to missing related objects, while also being efficient with database queries.
|
|
210
|
+
|
|
211
|
+
## 🎯 Lambda Conditions and Anonymous Functions
|
|
212
|
+
|
|
213
|
+
`django-bulk-hooks` supports using anonymous functions (lambda functions) and custom callables as conditions, giving you maximum flexibility for complex filtering logic.
|
|
214
|
+
|
|
215
|
+
### Using LambdaCondition
|
|
216
|
+
|
|
217
|
+
The `LambdaCondition` class allows you to use lambda functions or any callable as a condition:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from django_bulk_hooks import LambdaCondition
|
|
221
|
+
|
|
222
|
+
class ProductHandler:
|
|
223
|
+
# Simple lambda condition
|
|
224
|
+
@hook(Product, "after_create", condition=LambdaCondition(
|
|
225
|
+
lambda instance: instance.price > 100
|
|
226
|
+
))
|
|
227
|
+
def handle_expensive_products(self, new_records, old_records):
|
|
228
|
+
"""Handle products with price > 100"""
|
|
229
|
+
for product in new_records:
|
|
230
|
+
print(f"Expensive product: {product.name}")
|
|
231
|
+
|
|
232
|
+
# Lambda with multiple conditions
|
|
233
|
+
@hook(Product, "after_update", condition=LambdaCondition(
|
|
234
|
+
lambda instance: instance.price > 50 and instance.is_active and instance.stock_quantity > 0
|
|
235
|
+
))
|
|
236
|
+
def handle_available_expensive_products(self, new_records, old_records):
|
|
237
|
+
"""Handle active products with price > 50 and stock > 0"""
|
|
238
|
+
for product in new_records:
|
|
239
|
+
print(f"Available expensive product: {product.name}")
|
|
240
|
+
|
|
241
|
+
# Lambda comparing with original instance
|
|
242
|
+
@hook(Product, "after_update", condition=LambdaCondition(
|
|
243
|
+
lambda instance, original: original and instance.price > original.price * 1.5
|
|
244
|
+
))
|
|
245
|
+
def handle_significant_price_increases(self, new_records, old_records):
|
|
246
|
+
"""Handle products with >50% price increase"""
|
|
247
|
+
for new_product, old_product in zip(new_records, old_records):
|
|
248
|
+
if old_product:
|
|
249
|
+
increase = ((new_product.price - old_product.price) / old_product.price) * 100
|
|
250
|
+
print(f"Significant price increase: {new_product.name} +{increase:.1f}%")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Combining Lambda Conditions with Built-in Conditions
|
|
254
|
+
|
|
255
|
+
You can combine lambda conditions with built-in conditions using the `&` (AND) and `|` (OR) operators:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from django_bulk_hooks.conditions import HasChanged, IsEqual
|
|
259
|
+
|
|
260
|
+
class AdvancedProductHandler:
|
|
261
|
+
# Combine lambda with built-in conditions
|
|
262
|
+
@hook(Product, "after_update", condition=(
|
|
263
|
+
HasChanged("price") &
|
|
264
|
+
LambdaCondition(lambda instance: instance.price > 100)
|
|
265
|
+
))
|
|
266
|
+
def handle_expensive_price_changes(self, new_records, old_records):
|
|
267
|
+
"""Handle when expensive products have price changes"""
|
|
268
|
+
for new_product, old_product in zip(new_records, old_records):
|
|
269
|
+
print(f"Expensive product price changed: {new_product.name}")
|
|
270
|
+
|
|
271
|
+
# Complex combined conditions
|
|
272
|
+
@hook(Order, "after_update", condition=(
|
|
273
|
+
LambdaCondition(lambda instance: instance.status == 'completed') &
|
|
274
|
+
LambdaCondition(lambda instance, original: original and instance.total_amount > original.total_amount)
|
|
275
|
+
))
|
|
276
|
+
def handle_completed_orders_with_increased_amount(self, new_records, old_records):
|
|
277
|
+
"""Handle completed orders that had amount increases"""
|
|
278
|
+
for new_order, old_order in zip(new_records, old_records):
|
|
279
|
+
if old_order:
|
|
280
|
+
increase = new_order.total_amount - old_order.total_amount
|
|
281
|
+
print(f"Completed order with amount increase: {new_order.customer_name} +${increase}")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Custom Condition Classes
|
|
285
|
+
|
|
286
|
+
For reusable logic, you can create custom condition classes:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from django_bulk_hooks.conditions import HookCondition
|
|
290
|
+
|
|
291
|
+
class IsPremiumProduct(HookCondition):
|
|
292
|
+
def check(self, instance, original_instance=None):
|
|
293
|
+
return (
|
|
294
|
+
instance.price > 200 and
|
|
295
|
+
instance.rating >= 4.0 and
|
|
296
|
+
instance.is_active
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def get_required_fields(self):
|
|
300
|
+
return {'price', 'rating', 'is_active'}
|
|
301
|
+
|
|
302
|
+
class ProductHandler:
|
|
303
|
+
@hook(Product, "after_create", condition=IsPremiumProduct())
|
|
304
|
+
def handle_premium_products(self, new_records, old_records):
|
|
305
|
+
"""Handle premium products"""
|
|
306
|
+
for product in new_records:
|
|
307
|
+
print(f"Premium product: {product.name}")
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Lambda Conditions with Required Fields
|
|
311
|
+
|
|
312
|
+
For optimization, you can specify which fields your lambda condition depends on:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class OptimizedProductHandler:
|
|
316
|
+
@hook(Product, "after_update", condition=LambdaCondition(
|
|
317
|
+
lambda instance: instance.price > 100 and instance.category == 'electronics',
|
|
318
|
+
required_fields={'price', 'category'}
|
|
319
|
+
))
|
|
320
|
+
def handle_expensive_electronics(self, new_records, old_records):
|
|
321
|
+
"""Handle expensive electronics products"""
|
|
322
|
+
for product in new_records:
|
|
323
|
+
print(f"Expensive electronics: {product.name}")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Best Practices for Lambda Conditions
|
|
327
|
+
|
|
328
|
+
1. **Keep lambdas simple** - Complex logic should be moved to custom condition classes
|
|
329
|
+
2. **Handle None values** - Always check for None before performing operations
|
|
330
|
+
3. **Specify required fields** - This helps with query optimization
|
|
331
|
+
4. **Use descriptive names** - Make your lambda conditions self-documenting
|
|
332
|
+
5. **Test thoroughly** - Lambda conditions can be harder to debug than named functions
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
# ✅ GOOD: Simple, clear lambda
|
|
336
|
+
condition = LambdaCondition(lambda instance: instance.price > 100)
|
|
337
|
+
|
|
338
|
+
# ✅ GOOD: Handles None values
|
|
339
|
+
condition = LambdaCondition(
|
|
340
|
+
lambda instance: instance.price is not None and instance.price > 100
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# ❌ AVOID: Complex logic in lambda
|
|
344
|
+
condition = LambdaCondition(
|
|
345
|
+
lambda instance: (
|
|
346
|
+
instance.price > 100 and
|
|
347
|
+
instance.category in ['electronics', 'computers'] and
|
|
348
|
+
instance.stock_quantity > 0 and
|
|
349
|
+
instance.rating >= 4.0 and
|
|
350
|
+
instance.is_active and
|
|
351
|
+
instance.created_at > datetime.now() - timedelta(days=30)
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# ✅ BETTER: Use custom condition class for complex logic
|
|
356
|
+
class IsRecentExpensiveElectronics(HookCondition):
|
|
357
|
+
def check(self, instance, original_instance=None):
|
|
358
|
+
return (
|
|
359
|
+
instance.price > 100 and
|
|
360
|
+
instance.category in ['electronics', 'computers'] and
|
|
361
|
+
instance.stock_quantity > 0 and
|
|
362
|
+
instance.rating >= 4.0 and
|
|
363
|
+
instance.is_active and
|
|
364
|
+
instance.created_at > datetime.now() - timedelta(days=30)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def get_required_fields(self):
|
|
368
|
+
return {'price', 'category', 'stock_quantity', 'rating', 'is_active', 'created_at'}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## 🔧 Best Practices for Related Objects
|
|
@@ -8,6 +8,7 @@ from django_bulk_hooks.conditions import (
|
|
|
8
8
|
IsLessThan,
|
|
9
9
|
IsLessThanOrEqual,
|
|
10
10
|
IsNotEqual,
|
|
11
|
+
LambdaCondition,
|
|
11
12
|
WasEqual,
|
|
12
13
|
is_field_set,
|
|
13
14
|
safe_get_related_attr,
|
|
@@ -57,4 +58,5 @@ __all__ = [
|
|
|
57
58
|
"IsLessThan",
|
|
58
59
|
"IsGreaterThanOrEqual",
|
|
59
60
|
"IsLessThanOrEqual",
|
|
61
|
+
"LambdaCondition",
|
|
60
62
|
]
|
|
@@ -314,3 +314,45 @@ class IsLessThanOrEqual(HookCondition):
|
|
|
314
314
|
|
|
315
315
|
def get_required_fields(self):
|
|
316
316
|
return {self.field_name}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class LambdaCondition(HookCondition):
|
|
320
|
+
"""
|
|
321
|
+
A condition that uses a lambda function or any callable.
|
|
322
|
+
|
|
323
|
+
This makes it easy to create custom conditions inline without defining
|
|
324
|
+
a full class.
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
# Simple lambda condition
|
|
328
|
+
condition = LambdaCondition(lambda instance: instance.price > 100)
|
|
329
|
+
|
|
330
|
+
# Lambda with original instance
|
|
331
|
+
condition = LambdaCondition(
|
|
332
|
+
lambda instance, original: instance.price > original.price
|
|
333
|
+
)
|
|
334
|
+
|
|
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
|
+
|
|
347
|
+
Args:
|
|
348
|
+
func: A callable that takes (instance, original_instance=None) and returns bool
|
|
349
|
+
required_fields: Optional set of field names this condition depends on
|
|
350
|
+
"""
|
|
351
|
+
self.func = func
|
|
352
|
+
self._required_fields = required_fields or set()
|
|
353
|
+
|
|
354
|
+
def check(self, instance, original_instance=None):
|
|
355
|
+
return self.func(instance, original_instance)
|
|
356
|
+
|
|
357
|
+
def get_required_fields(self):
|
|
358
|
+
return self._required_fields
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
1
4
|
from django.db import models, transaction
|
|
5
|
+
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
|
|
2
6
|
|
|
3
7
|
from django_bulk_hooks.constants import (
|
|
4
8
|
AFTER_CREATE,
|
|
@@ -14,9 +18,6 @@ from django_bulk_hooks.constants import (
|
|
|
14
18
|
from django_bulk_hooks.context import HookContext
|
|
15
19
|
from django_bulk_hooks.engine import run
|
|
16
20
|
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
|
|
|
21
22
|
|
|
22
23
|
@contextlib.contextmanager
|
|
@@ -26,7 +27,7 @@ def patch_foreign_key_behavior():
|
|
|
26
27
|
RelatedObjectDoesNotExist when accessing an unset foreign key field.
|
|
27
28
|
"""
|
|
28
29
|
original_get = ForwardManyToOneDescriptor.__get__
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
@wraps(original_get)
|
|
31
32
|
def safe_get(self, instance, cls=None):
|
|
32
33
|
if instance is None:
|
|
@@ -35,7 +36,7 @@ def patch_foreign_key_behavior():
|
|
|
35
36
|
return original_get(self, instance, cls)
|
|
36
37
|
except self.RelatedObjectDoesNotExist:
|
|
37
38
|
return None
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
# Patch the descriptor
|
|
40
41
|
ForwardManyToOneDescriptor.__get__ = safe_get
|
|
41
42
|
try:
|
|
@@ -63,7 +64,7 @@ class HookModelMixin(models.Model):
|
|
|
63
64
|
# Skip hook validation during admin form validation
|
|
64
65
|
# This prevents RelatedObjectDoesNotExist errors when Django hasn't
|
|
65
66
|
# fully set up the object's relationships yet
|
|
66
|
-
if hasattr(self,
|
|
67
|
+
if hasattr(self, "_state") and getattr(self._state, "validating", False):
|
|
67
68
|
return
|
|
68
69
|
|
|
69
70
|
# Determine if this is a create or update operation
|
|
@@ -80,7 +81,9 @@ class HookModelMixin(models.Model):
|
|
|
80
81
|
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
81
82
|
ctx = HookContext(self.__class__)
|
|
82
83
|
with patch_foreign_key_behavior():
|
|
83
|
-
run(
|
|
84
|
+
run(
|
|
85
|
+
self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx
|
|
86
|
+
)
|
|
84
87
|
except self.__class__.DoesNotExist:
|
|
85
88
|
# If the old instance doesn't exist, treat as create
|
|
86
89
|
ctx = HookContext(self.__class__)
|
|
@@ -91,27 +94,40 @@ class HookModelMixin(models.Model):
|
|
|
91
94
|
is_create = self.pk is None
|
|
92
95
|
ctx = HookContext(self.__class__)
|
|
93
96
|
|
|
94
|
-
#
|
|
95
|
-
super().save(*args, **kwargs)
|
|
96
|
-
|
|
97
|
-
# Then run our hooks with the validated data
|
|
97
|
+
# Run BEFORE hooks before saving to allow field modifications
|
|
98
98
|
with patch_foreign_key_behavior():
|
|
99
99
|
if is_create:
|
|
100
100
|
# For create operations
|
|
101
101
|
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
102
102
|
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
103
|
-
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
104
103
|
else:
|
|
105
104
|
# For update operations
|
|
106
105
|
try:
|
|
107
106
|
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
108
|
-
run(
|
|
107
|
+
run(
|
|
108
|
+
self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx
|
|
109
|
+
)
|
|
109
110
|
run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
110
|
-
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
111
111
|
except self.__class__.DoesNotExist:
|
|
112
112
|
# If the old instance doesn't exist, treat as create
|
|
113
113
|
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
114
114
|
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
115
|
+
|
|
116
|
+
# Now let Django save with any modifications from BEFORE hooks
|
|
117
|
+
super().save(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
# Then run AFTER hooks
|
|
120
|
+
with patch_foreign_key_behavior():
|
|
121
|
+
if is_create:
|
|
122
|
+
# For create operations
|
|
123
|
+
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
124
|
+
else:
|
|
125
|
+
# For update operations
|
|
126
|
+
try:
|
|
127
|
+
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
128
|
+
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
129
|
+
except self.__class__.DoesNotExist:
|
|
130
|
+
# If the old instance doesn't exist, treat as create
|
|
115
131
|
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
116
132
|
|
|
117
133
|
return self
|
|
@@ -125,5 +141,5 @@ class HookModelMixin(models.Model):
|
|
|
125
141
|
run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
|
|
126
142
|
result = super().delete(*args, **kwargs)
|
|
127
143
|
run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
|
|
128
|
-
|
|
144
|
+
|
|
129
145
|
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.100"
|
|
4
4
|
description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
|
|
5
5
|
authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
|
|
6
6
|
readme = "README.md"
|
|
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
|