django-bulk-hooks 0.1.117__tar.gz → 0.1.119__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.117 → django_bulk_hooks-0.1.119}/PKG-INFO +3 -3
- django_bulk_hooks-0.1.119/django_bulk_hooks/manager.py +201 -0
- django_bulk_hooks-0.1.117/django_bulk_hooks/manager.py → django_bulk_hooks-0.1.119/django_bulk_hooks/queryset.py +140 -153
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.117/django_bulk_hooks/queryset.py +0 -44
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/LICENSE +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/README.md +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.119}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.119
|
|
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
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: django,bulk,hooks
|
|
8
7
|
Author: Konrad Beck
|
|
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
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
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from django.db import models, transaction
|
|
2
|
+
|
|
3
|
+
from django_bulk_hooks import engine
|
|
4
|
+
from django_bulk_hooks.constants import (
|
|
5
|
+
AFTER_CREATE,
|
|
6
|
+
AFTER_DELETE,
|
|
7
|
+
AFTER_UPDATE,
|
|
8
|
+
BEFORE_CREATE,
|
|
9
|
+
BEFORE_DELETE,
|
|
10
|
+
BEFORE_UPDATE,
|
|
11
|
+
VALIDATE_CREATE,
|
|
12
|
+
VALIDATE_DELETE,
|
|
13
|
+
VALIDATE_UPDATE,
|
|
14
|
+
)
|
|
15
|
+
from django_bulk_hooks.context import HookContext
|
|
16
|
+
from django_bulk_hooks.queryset import HookQuerySet
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BulkHookManager(models.Manager):
|
|
20
|
+
CHUNK_SIZE = 200
|
|
21
|
+
|
|
22
|
+
def get_queryset(self):
|
|
23
|
+
return HookQuerySet(self.model, using=self._db)
|
|
24
|
+
|
|
25
|
+
@transaction.atomic
|
|
26
|
+
def bulk_update(
|
|
27
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
28
|
+
):
|
|
29
|
+
if not objs:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
model_cls = self.model
|
|
33
|
+
|
|
34
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
35
|
+
raise TypeError(
|
|
36
|
+
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if not bypass_hooks:
|
|
40
|
+
# Load originals for hook comparison and ensure they match the order of new instances
|
|
41
|
+
original_map = {
|
|
42
|
+
obj.pk: obj
|
|
43
|
+
for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
|
|
44
|
+
}
|
|
45
|
+
originals = [original_map.get(obj.pk) for obj in objs]
|
|
46
|
+
|
|
47
|
+
ctx = HookContext(model_cls)
|
|
48
|
+
|
|
49
|
+
# Run validation hooks first
|
|
50
|
+
if not bypass_validation:
|
|
51
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
52
|
+
|
|
53
|
+
# Then run business logic hooks
|
|
54
|
+
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
55
|
+
|
|
56
|
+
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
57
|
+
modified_fields = self._detect_modified_fields(objs, originals)
|
|
58
|
+
if modified_fields:
|
|
59
|
+
# Convert to set for efficient union operation
|
|
60
|
+
fields_set = set(fields)
|
|
61
|
+
fields_set.update(modified_fields)
|
|
62
|
+
fields = list(fields_set)
|
|
63
|
+
|
|
64
|
+
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
65
|
+
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
66
|
+
# Call the base implementation to avoid re-triggering this method
|
|
67
|
+
super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
|
|
68
|
+
|
|
69
|
+
if not bypass_hooks:
|
|
70
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
71
|
+
|
|
72
|
+
return objs
|
|
73
|
+
|
|
74
|
+
@transaction.atomic
|
|
75
|
+
def bulk_create(
|
|
76
|
+
self,
|
|
77
|
+
objs,
|
|
78
|
+
batch_size=None,
|
|
79
|
+
ignore_conflicts=False,
|
|
80
|
+
update_conflicts=False,
|
|
81
|
+
update_fields=None,
|
|
82
|
+
unique_fields=None,
|
|
83
|
+
bypass_hooks=False,
|
|
84
|
+
bypass_validation=False,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Delegate to QuerySet's bulk_create implementation.
|
|
88
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
89
|
+
"""
|
|
90
|
+
return self.get_queryset().bulk_create(
|
|
91
|
+
objs,
|
|
92
|
+
batch_size=batch_size,
|
|
93
|
+
ignore_conflicts=ignore_conflicts,
|
|
94
|
+
update_conflicts=update_conflicts,
|
|
95
|
+
update_fields=update_fields,
|
|
96
|
+
unique_fields=unique_fields,
|
|
97
|
+
bypass_hooks=bypass_hooks,
|
|
98
|
+
bypass_validation=bypass_validation,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@transaction.atomic
|
|
102
|
+
def bulk_delete(
|
|
103
|
+
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
104
|
+
):
|
|
105
|
+
if not objs:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
model_cls = self.model
|
|
109
|
+
|
|
110
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
111
|
+
raise TypeError(
|
|
112
|
+
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
ctx = HookContext(model_cls)
|
|
116
|
+
|
|
117
|
+
if not bypass_hooks:
|
|
118
|
+
# Run validation hooks first
|
|
119
|
+
if not bypass_validation:
|
|
120
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
121
|
+
|
|
122
|
+
# Then run business logic hooks
|
|
123
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
124
|
+
|
|
125
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
126
|
+
|
|
127
|
+
# Use base manager for the actual deletion to prevent recursion
|
|
128
|
+
# The hooks have already been fired above, so we don't need them again
|
|
129
|
+
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
130
|
+
|
|
131
|
+
if not bypass_hooks:
|
|
132
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
133
|
+
|
|
134
|
+
return objs
|
|
135
|
+
|
|
136
|
+
@transaction.atomic
|
|
137
|
+
def update(self, **kwargs):
|
|
138
|
+
objs = list(self.all())
|
|
139
|
+
if not objs:
|
|
140
|
+
return 0
|
|
141
|
+
for key, value in kwargs.items():
|
|
142
|
+
for obj in objs:
|
|
143
|
+
setattr(obj, key, value)
|
|
144
|
+
self.bulk_update(objs, fields=list(kwargs.keys()))
|
|
145
|
+
return len(objs)
|
|
146
|
+
|
|
147
|
+
@transaction.atomic
|
|
148
|
+
def delete(self):
|
|
149
|
+
objs = list(self.all())
|
|
150
|
+
if not objs:
|
|
151
|
+
return 0
|
|
152
|
+
self.bulk_delete(objs)
|
|
153
|
+
return len(objs)
|
|
154
|
+
|
|
155
|
+
@transaction.atomic
|
|
156
|
+
def save(self, obj):
|
|
157
|
+
if obj.pk:
|
|
158
|
+
self.bulk_update(
|
|
159
|
+
[obj],
|
|
160
|
+
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
self.bulk_create([obj])
|
|
164
|
+
return obj
|
|
165
|
+
|
|
166
|
+
def _detect_modified_fields(self, new_instances, original_instances):
|
|
167
|
+
"""
|
|
168
|
+
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
169
|
+
new instances with their original values.
|
|
170
|
+
"""
|
|
171
|
+
if not original_instances:
|
|
172
|
+
return set()
|
|
173
|
+
|
|
174
|
+
modified_fields = set()
|
|
175
|
+
|
|
176
|
+
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
177
|
+
for new_instance, original in zip(new_instances, original_instances):
|
|
178
|
+
if new_instance.pk is None or original is None:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Compare all fields to detect changes
|
|
182
|
+
for field in new_instance._meta.fields:
|
|
183
|
+
if field.name == "id":
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
new_value = getattr(new_instance, field.name)
|
|
187
|
+
original_value = getattr(original, field.name)
|
|
188
|
+
|
|
189
|
+
# Handle different field types appropriately
|
|
190
|
+
if field.is_relation:
|
|
191
|
+
# For foreign keys, compare the pk values
|
|
192
|
+
new_pk = new_value.pk if new_value else None
|
|
193
|
+
original_pk = original_value.pk if original_value else None
|
|
194
|
+
if new_pk != original_pk:
|
|
195
|
+
modified_fields.add(field.name)
|
|
196
|
+
else:
|
|
197
|
+
# For regular fields, use direct comparison
|
|
198
|
+
if new_value != original_value:
|
|
199
|
+
modified_fields.add(field.name)
|
|
200
|
+
|
|
201
|
+
return modified_fields
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
from django.db.models import AutoField
|
|
1
|
+
from django.db import models, transaction, connections
|
|
2
|
+
from django.db.models import AutoField, Q, Max
|
|
3
|
+
from django.db import NotSupportedError
|
|
4
|
+
from django.db.models.constants import OnConflict
|
|
5
|
+
from django.db.models.expressions import DatabaseDefault
|
|
6
|
+
import operator
|
|
7
|
+
from functools import reduce
|
|
3
8
|
|
|
4
9
|
from django_bulk_hooks import engine
|
|
5
10
|
from django_bulk_hooks.constants import (
|
|
@@ -14,63 +19,48 @@ from django_bulk_hooks.constants import (
|
|
|
14
19
|
VALIDATE_UPDATE,
|
|
15
20
|
)
|
|
16
21
|
from django_bulk_hooks.context import HookContext
|
|
17
|
-
from django_bulk_hooks.queryset import HookQuerySet
|
|
18
22
|
|
|
19
23
|
|
|
20
|
-
class
|
|
24
|
+
class HookQuerySet(models.QuerySet):
|
|
21
25
|
CHUNK_SIZE = 200
|
|
22
26
|
|
|
23
|
-
def get_queryset(self):
|
|
24
|
-
return HookQuerySet(self.model, using=self._db)
|
|
25
|
-
|
|
26
27
|
@transaction.atomic
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
):
|
|
28
|
+
def delete(self):
|
|
29
|
+
objs = list(self)
|
|
30
30
|
if not objs:
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
model_cls = self.model
|
|
34
|
-
|
|
35
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
36
|
-
raise TypeError(
|
|
37
|
-
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
38
|
-
)
|
|
31
|
+
return 0
|
|
32
|
+
return self.model.objects.bulk_delete(objs)
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
originals = [original_map.get(obj.pk) for obj in objs]
|
|
34
|
+
@transaction.atomic
|
|
35
|
+
def update(self, **kwargs):
|
|
36
|
+
instances = list(self)
|
|
37
|
+
if not instances:
|
|
38
|
+
return 0
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
model_cls = self.model
|
|
41
|
+
pks = [obj.pk for obj in instances]
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
# Load originals for hook comparison and ensure they match the order of instances
|
|
44
|
+
original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
|
|
45
|
+
originals = [original_map.get(obj.pk) for obj in instances]
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
# Apply field updates to instances
|
|
48
|
+
for obj in instances:
|
|
49
|
+
for field, value in kwargs.items():
|
|
50
|
+
setattr(obj, field, value)
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Convert to set for efficient union operation
|
|
61
|
-
fields_set = set(fields)
|
|
62
|
-
fields_set.update(modified_fields)
|
|
63
|
-
fields = list(fields_set)
|
|
52
|
+
# Run BEFORE_UPDATE hooks
|
|
53
|
+
ctx = HookContext(model_cls)
|
|
54
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
|
|
56
|
+
# Use Django's built-in update logic directly
|
|
57
|
+
queryset = self.model.objects.filter(pk__in=pks)
|
|
58
|
+
update_count = queryset.update(**kwargs)
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
# Run AFTER_UPDATE hooks
|
|
61
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
72
62
|
|
|
73
|
-
return
|
|
63
|
+
return update_count
|
|
74
64
|
|
|
75
65
|
@transaction.atomic
|
|
76
66
|
def bulk_create(
|
|
@@ -86,7 +76,7 @@ class BulkHookManager(models.Manager):
|
|
|
86
76
|
):
|
|
87
77
|
"""
|
|
88
78
|
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
89
|
-
but supports multi-table inheritance (MTI) models. All arguments are supported and
|
|
79
|
+
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
90
80
|
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
91
81
|
"""
|
|
92
82
|
model_cls = self.model
|
|
@@ -94,10 +84,11 @@ class BulkHookManager(models.Manager):
|
|
|
94
84
|
if batch_size is not None and batch_size <= 0:
|
|
95
85
|
raise ValueError("Batch size must be a positive integer.")
|
|
96
86
|
|
|
97
|
-
# Check
|
|
87
|
+
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
88
|
+
is_mti = False
|
|
98
89
|
for parent in model_cls._meta.all_parents:
|
|
99
90
|
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
100
|
-
|
|
91
|
+
is_mti = True
|
|
101
92
|
break
|
|
102
93
|
|
|
103
94
|
if not objs:
|
|
@@ -108,9 +99,6 @@ class BulkHookManager(models.Manager):
|
|
|
108
99
|
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
109
100
|
)
|
|
110
101
|
|
|
111
|
-
# Set auto_now_add/auto_now fields before DB ops
|
|
112
|
-
self._set_auto_now_fields(objs, model_cls)
|
|
113
|
-
|
|
114
102
|
# Fire hooks before DB ops
|
|
115
103
|
if not bypass_hooks:
|
|
116
104
|
ctx = HookContext(model_cls)
|
|
@@ -118,12 +106,10 @@ class BulkHookManager(models.Manager):
|
|
|
118
106
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
119
107
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
120
108
|
|
|
121
|
-
# MTI
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Pass through all supported arguments
|
|
126
|
-
result = super(models.Manager, self).bulk_create(
|
|
109
|
+
# For MTI models, we need to handle them specially
|
|
110
|
+
if is_mti:
|
|
111
|
+
# Use our MTI-specific logic
|
|
112
|
+
result = self._mti_bulk_create(
|
|
127
113
|
objs,
|
|
128
114
|
batch_size=batch_size,
|
|
129
115
|
ignore_conflicts=ignore_conflicts,
|
|
@@ -132,22 +118,105 @@ class BulkHookManager(models.Manager):
|
|
|
132
118
|
unique_fields=unique_fields,
|
|
133
119
|
)
|
|
134
120
|
else:
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
121
|
+
# For single-table models, use Django's built-in bulk_create
|
|
122
|
+
# but we need to call it on the base manager to avoid recursion
|
|
123
|
+
result = model_cls._base_manager.bulk_create(
|
|
124
|
+
objs,
|
|
125
|
+
batch_size=batch_size,
|
|
126
|
+
ignore_conflicts=ignore_conflicts,
|
|
127
|
+
update_conflicts=update_conflicts,
|
|
128
|
+
update_fields=update_fields,
|
|
129
|
+
unique_fields=unique_fields,
|
|
143
130
|
)
|
|
144
131
|
|
|
145
132
|
if not bypass_hooks:
|
|
146
|
-
engine.run(model_cls, AFTER_CREATE,
|
|
133
|
+
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
147
134
|
|
|
148
135
|
return result
|
|
149
136
|
|
|
150
|
-
|
|
137
|
+
@transaction.atomic
|
|
138
|
+
def bulk_update(
|
|
139
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
140
|
+
):
|
|
141
|
+
if not objs:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
model_cls = self.model
|
|
145
|
+
|
|
146
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
147
|
+
raise TypeError(
|
|
148
|
+
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if not bypass_hooks:
|
|
152
|
+
# Load originals for hook comparison and ensure they match the order of new instances
|
|
153
|
+
original_map = {
|
|
154
|
+
obj.pk: obj
|
|
155
|
+
for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
|
|
156
|
+
}
|
|
157
|
+
originals = [original_map.get(obj.pk) for obj in objs]
|
|
158
|
+
|
|
159
|
+
ctx = HookContext(model_cls)
|
|
160
|
+
|
|
161
|
+
# Run validation hooks first
|
|
162
|
+
if not bypass_validation:
|
|
163
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
164
|
+
|
|
165
|
+
# Then run business logic hooks
|
|
166
|
+
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
167
|
+
|
|
168
|
+
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
169
|
+
modified_fields = self._detect_modified_fields(objs, originals)
|
|
170
|
+
if modified_fields:
|
|
171
|
+
# Convert to set for efficient union operation
|
|
172
|
+
fields_set = set(fields)
|
|
173
|
+
fields_set.update(modified_fields)
|
|
174
|
+
fields = list(fields_set)
|
|
175
|
+
|
|
176
|
+
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
177
|
+
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
178
|
+
# Call the base implementation to avoid re-triggering this method
|
|
179
|
+
super().bulk_update(chunk, fields, **kwargs)
|
|
180
|
+
|
|
181
|
+
if not bypass_hooks:
|
|
182
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
183
|
+
|
|
184
|
+
return objs
|
|
185
|
+
|
|
186
|
+
@transaction.atomic
|
|
187
|
+
def bulk_delete(self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False):
|
|
188
|
+
if not objs:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
model_cls = self.model
|
|
192
|
+
|
|
193
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
194
|
+
raise TypeError(
|
|
195
|
+
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
ctx = HookContext(model_cls)
|
|
199
|
+
|
|
200
|
+
if not bypass_hooks:
|
|
201
|
+
# Run validation hooks first
|
|
202
|
+
if not bypass_validation:
|
|
203
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
204
|
+
|
|
205
|
+
# Then run business logic hooks
|
|
206
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
207
|
+
|
|
208
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
209
|
+
|
|
210
|
+
# Use base manager for the actual deletion to prevent recursion
|
|
211
|
+
# The hooks have already been fired above, so we don't need them again
|
|
212
|
+
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
213
|
+
|
|
214
|
+
if not bypass_hooks:
|
|
215
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
216
|
+
|
|
217
|
+
return objs
|
|
218
|
+
|
|
219
|
+
# --- Private helper methods ---
|
|
151
220
|
|
|
152
221
|
def _detect_modified_fields(self, new_instances, original_instances):
|
|
153
222
|
"""
|
|
@@ -205,19 +274,19 @@ class BulkHookManager(models.Manager):
|
|
|
205
274
|
chain.reverse()
|
|
206
275
|
return chain
|
|
207
276
|
|
|
208
|
-
def _mti_bulk_create(self, objs, inheritance_chain, **kwargs):
|
|
277
|
+
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
209
278
|
"""
|
|
210
279
|
Implements workaround: individual saves for parents, bulk create for child.
|
|
211
280
|
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
212
281
|
"""
|
|
282
|
+
if inheritance_chain is None:
|
|
283
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
284
|
+
|
|
213
285
|
batch_size = kwargs.get("batch_size") or len(objs)
|
|
214
286
|
created_objects = []
|
|
215
287
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
216
288
|
for i in range(0, len(objs), batch_size):
|
|
217
289
|
batch = objs[i : i + batch_size]
|
|
218
|
-
# Set auto_now fields for each model in the chain
|
|
219
|
-
for model in inheritance_chain:
|
|
220
|
-
self._set_auto_now_fields(batch, model)
|
|
221
290
|
batch_result = self._process_mti_batch(
|
|
222
291
|
batch, inheritance_chain, **kwargs
|
|
223
292
|
)
|
|
@@ -311,85 +380,3 @@ class BulkHookManager(models.Manager):
|
|
|
311
380
|
if parent_link:
|
|
312
381
|
setattr(child_obj, parent_link.name, parent_instance)
|
|
313
382
|
return child_obj
|
|
314
|
-
|
|
315
|
-
def _set_auto_now_fields(self, objs, model):
|
|
316
|
-
"""
|
|
317
|
-
Set auto_now_add and auto_now fields on objects before bulk_create.
|
|
318
|
-
"""
|
|
319
|
-
from django.utils import timezone
|
|
320
|
-
|
|
321
|
-
now = timezone.now()
|
|
322
|
-
for obj in objs:
|
|
323
|
-
for field in model._meta.local_fields:
|
|
324
|
-
if (
|
|
325
|
-
getattr(field, "auto_now_add", False)
|
|
326
|
-
and getattr(obj, field.name, None) is None
|
|
327
|
-
):
|
|
328
|
-
setattr(obj, field.name, now)
|
|
329
|
-
if getattr(field, "auto_now", False):
|
|
330
|
-
setattr(obj, field.name, now)
|
|
331
|
-
|
|
332
|
-
@transaction.atomic
|
|
333
|
-
def bulk_delete(
|
|
334
|
-
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
335
|
-
):
|
|
336
|
-
if not objs:
|
|
337
|
-
return []
|
|
338
|
-
|
|
339
|
-
model_cls = self.model
|
|
340
|
-
|
|
341
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
342
|
-
raise TypeError(
|
|
343
|
-
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
ctx = HookContext(model_cls)
|
|
347
|
-
|
|
348
|
-
if not bypass_hooks:
|
|
349
|
-
# Run validation hooks first
|
|
350
|
-
if not bypass_validation:
|
|
351
|
-
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
352
|
-
|
|
353
|
-
# Then run business logic hooks
|
|
354
|
-
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
355
|
-
|
|
356
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
357
|
-
|
|
358
|
-
# Use base manager for the actual deletion to prevent recursion
|
|
359
|
-
# The hooks have already been fired above, so we don't need them again
|
|
360
|
-
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
361
|
-
|
|
362
|
-
if not bypass_hooks:
|
|
363
|
-
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
364
|
-
|
|
365
|
-
return objs
|
|
366
|
-
|
|
367
|
-
@transaction.atomic
|
|
368
|
-
def update(self, **kwargs):
|
|
369
|
-
objs = list(self.all())
|
|
370
|
-
if not objs:
|
|
371
|
-
return 0
|
|
372
|
-
for key, value in kwargs.items():
|
|
373
|
-
for obj in objs:
|
|
374
|
-
setattr(obj, key, value)
|
|
375
|
-
self.bulk_update(objs, fields=list(kwargs.keys()))
|
|
376
|
-
return len(objs)
|
|
377
|
-
|
|
378
|
-
@transaction.atomic
|
|
379
|
-
def delete(self):
|
|
380
|
-
objs = list(self.all())
|
|
381
|
-
if not objs:
|
|
382
|
-
return 0
|
|
383
|
-
self.bulk_delete(objs)
|
|
384
|
-
return len(objs)
|
|
385
|
-
|
|
386
|
-
@transaction.atomic
|
|
387
|
-
def save(self, obj):
|
|
388
|
-
if obj.pk:
|
|
389
|
-
self.bulk_update(
|
|
390
|
-
[obj],
|
|
391
|
-
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
392
|
-
)
|
|
393
|
-
else:
|
|
394
|
-
self.bulk_create([obj])
|
|
395
|
-
return obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.119"
|
|
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"
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class HookQuerySet(models.QuerySet):
|
|
5
|
-
@transaction.atomic
|
|
6
|
-
def delete(self):
|
|
7
|
-
objs = list(self)
|
|
8
|
-
if not objs:
|
|
9
|
-
return 0
|
|
10
|
-
return self.model.objects.bulk_delete(objs)
|
|
11
|
-
|
|
12
|
-
@transaction.atomic
|
|
13
|
-
def update(self, **kwargs):
|
|
14
|
-
instances = list(self)
|
|
15
|
-
if not instances:
|
|
16
|
-
return 0
|
|
17
|
-
|
|
18
|
-
model_cls = self.model
|
|
19
|
-
pks = [obj.pk for obj in instances]
|
|
20
|
-
|
|
21
|
-
# Load originals for hook comparison and ensure they match the order of instances
|
|
22
|
-
original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
|
|
23
|
-
originals = [original_map.get(obj.pk) for obj in instances]
|
|
24
|
-
|
|
25
|
-
# Apply field updates to instances
|
|
26
|
-
for obj in instances:
|
|
27
|
-
for field, value in kwargs.items():
|
|
28
|
-
setattr(obj, field, value)
|
|
29
|
-
|
|
30
|
-
# Run BEFORE_UPDATE hooks
|
|
31
|
-
from django_bulk_hooks import engine
|
|
32
|
-
from django_bulk_hooks.context import HookContext
|
|
33
|
-
|
|
34
|
-
ctx = HookContext(model_cls)
|
|
35
|
-
engine.run(model_cls, "before_update", instances, originals, ctx=ctx)
|
|
36
|
-
|
|
37
|
-
# Use Django's built-in update logic directly
|
|
38
|
-
queryset = self.model.objects.filter(pk__in=pks)
|
|
39
|
-
update_count = queryset.update(**kwargs)
|
|
40
|
-
|
|
41
|
-
# Run AFTER_UPDATE hooks
|
|
42
|
-
engine.run(model_cls, "after_update", instances, originals, ctx=ctx)
|
|
43
|
-
|
|
44
|
-
return update_count
|
|
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
|