django-bulk-hooks 0.1.118__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.118 → django_bulk_hooks-0.1.119}/PKG-INFO +1 -1
- django_bulk_hooks-0.1.119/django_bulk_hooks/manager.py +201 -0
- django_bulk_hooks-0.1.118/django_bulk_hooks/manager.py → django_bulk_hooks-0.1.119/django_bulk_hooks/queryset.py +139 -208
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.118/django_bulk_hooks/queryset.py +0 -44
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/LICENSE +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/README.md +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.118 → django_bulk_hooks-0.1.119}/django_bulk_hooks/registry.py +0 -0
|
@@ -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
1
|
from django.db import models, transaction, connections
|
|
2
|
-
from django.db.models import AutoField
|
|
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,14 +19,120 @@ 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
|
-
|
|
24
|
-
|
|
27
|
+
@transaction.atomic
|
|
28
|
+
def delete(self):
|
|
29
|
+
objs = list(self)
|
|
30
|
+
if not objs:
|
|
31
|
+
return 0
|
|
32
|
+
return self.model.objects.bulk_delete(objs)
|
|
33
|
+
|
|
34
|
+
@transaction.atomic
|
|
35
|
+
def update(self, **kwargs):
|
|
36
|
+
instances = list(self)
|
|
37
|
+
if not instances:
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
model_cls = self.model
|
|
41
|
+
pks = [obj.pk for obj in instances]
|
|
42
|
+
|
|
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]
|
|
46
|
+
|
|
47
|
+
# Apply field updates to instances
|
|
48
|
+
for obj in instances:
|
|
49
|
+
for field, value in kwargs.items():
|
|
50
|
+
setattr(obj, field, value)
|
|
51
|
+
|
|
52
|
+
# Run BEFORE_UPDATE hooks
|
|
53
|
+
ctx = HookContext(model_cls)
|
|
54
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
55
|
+
|
|
56
|
+
# Use Django's built-in update logic directly
|
|
57
|
+
queryset = self.model.objects.filter(pk__in=pks)
|
|
58
|
+
update_count = queryset.update(**kwargs)
|
|
59
|
+
|
|
60
|
+
# Run AFTER_UPDATE hooks
|
|
61
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
62
|
+
|
|
63
|
+
return update_count
|
|
64
|
+
|
|
65
|
+
@transaction.atomic
|
|
66
|
+
def bulk_create(
|
|
67
|
+
self,
|
|
68
|
+
objs,
|
|
69
|
+
batch_size=None,
|
|
70
|
+
ignore_conflicts=False,
|
|
71
|
+
update_conflicts=False,
|
|
72
|
+
update_fields=None,
|
|
73
|
+
unique_fields=None,
|
|
74
|
+
bypass_hooks=False,
|
|
75
|
+
bypass_validation=False,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
79
|
+
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
80
|
+
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
81
|
+
"""
|
|
82
|
+
model_cls = self.model
|
|
83
|
+
|
|
84
|
+
if batch_size is not None and batch_size <= 0:
|
|
85
|
+
raise ValueError("Batch size must be a positive integer.")
|
|
86
|
+
|
|
87
|
+
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
88
|
+
is_mti = False
|
|
89
|
+
for parent in model_cls._meta.all_parents:
|
|
90
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
91
|
+
is_mti = True
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not objs:
|
|
95
|
+
return objs
|
|
96
|
+
|
|
97
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
98
|
+
raise TypeError(
|
|
99
|
+
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Fire hooks before DB ops
|
|
103
|
+
if not bypass_hooks:
|
|
104
|
+
ctx = HookContext(model_cls)
|
|
105
|
+
if not bypass_validation:
|
|
106
|
+
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
107
|
+
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
108
|
+
|
|
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(
|
|
113
|
+
objs,
|
|
114
|
+
batch_size=batch_size,
|
|
115
|
+
ignore_conflicts=ignore_conflicts,
|
|
116
|
+
update_conflicts=update_conflicts,
|
|
117
|
+
update_fields=update_fields,
|
|
118
|
+
unique_fields=unique_fields,
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
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,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not bypass_hooks:
|
|
133
|
+
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
134
|
+
|
|
135
|
+
return result
|
|
25
136
|
|
|
26
137
|
@transaction.atomic
|
|
27
138
|
def bulk_update(
|
|
@@ -65,7 +176,7 @@ class BulkHookManager(models.Manager):
|
|
|
65
176
|
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
66
177
|
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
67
178
|
# Call the base implementation to avoid re-triggering this method
|
|
68
|
-
super(
|
|
179
|
+
super().bulk_update(chunk, fields, **kwargs)
|
|
69
180
|
|
|
70
181
|
if not bypass_hooks:
|
|
71
182
|
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
@@ -73,116 +184,39 @@ class BulkHookManager(models.Manager):
|
|
|
73
184
|
return objs
|
|
74
185
|
|
|
75
186
|
@transaction.atomic
|
|
76
|
-
def
|
|
77
|
-
self,
|
|
78
|
-
objs,
|
|
79
|
-
batch_size=None,
|
|
80
|
-
ignore_conflicts=False,
|
|
81
|
-
update_conflicts=False,
|
|
82
|
-
update_fields=None,
|
|
83
|
-
unique_fields=None,
|
|
84
|
-
bypass_hooks=False,
|
|
85
|
-
bypass_validation=False,
|
|
86
|
-
):
|
|
87
|
-
"""
|
|
88
|
-
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
|
|
90
|
-
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
91
|
-
"""
|
|
92
|
-
model_cls = self.model
|
|
93
|
-
|
|
94
|
-
if batch_size is not None and batch_size <= 0:
|
|
95
|
-
raise ValueError("Batch size must be a positive integer.")
|
|
96
|
-
|
|
97
|
-
# Check that the parents share the same concrete model with our model to detect inheritance pattern
|
|
98
|
-
# (Do NOT raise for MTI, just skip the exception)
|
|
99
|
-
for parent in model_cls._meta.all_parents:
|
|
100
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
101
|
-
# Do not raise, just continue
|
|
102
|
-
break
|
|
103
|
-
|
|
187
|
+
def bulk_delete(self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False):
|
|
104
188
|
if not objs:
|
|
105
|
-
return
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
model_cls = self.model
|
|
106
192
|
|
|
107
193
|
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
108
194
|
raise TypeError(
|
|
109
|
-
f"
|
|
195
|
+
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
110
196
|
)
|
|
111
197
|
|
|
112
|
-
|
|
113
|
-
self._set_auto_now_fields(objs, model_cls)
|
|
198
|
+
ctx = HookContext(model_cls)
|
|
114
199
|
|
|
115
|
-
# Fire hooks before DB ops
|
|
116
200
|
if not bypass_hooks:
|
|
117
|
-
|
|
201
|
+
# Run validation hooks first
|
|
118
202
|
if not bypass_validation:
|
|
119
|
-
engine.run(model_cls,
|
|
120
|
-
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
203
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
121
204
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
on_conflict = self._check_bulk_create_options(
|
|
131
|
-
ignore_conflicts,
|
|
132
|
-
update_conflicts,
|
|
133
|
-
update_fields,
|
|
134
|
-
unique_fields,
|
|
135
|
-
)
|
|
136
|
-
self._for_write = True
|
|
137
|
-
fields = [f for f in opts.concrete_fields if not f.generated]
|
|
138
|
-
objs = list(objs)
|
|
139
|
-
objs_with_pk, objs_without_pk = self._prepare_for_bulk_create(objs)
|
|
140
|
-
with transaction.atomic(using=self.db, savepoint=False):
|
|
141
|
-
self._handle_order_with_respect_to(objs)
|
|
142
|
-
if objs_with_pk:
|
|
143
|
-
returned_columns = self._batched_insert(
|
|
144
|
-
objs_with_pk,
|
|
145
|
-
fields,
|
|
146
|
-
batch_size,
|
|
147
|
-
on_conflict=on_conflict,
|
|
148
|
-
update_fields=update_fields,
|
|
149
|
-
unique_fields=unique_fields,
|
|
150
|
-
)
|
|
151
|
-
for obj_with_pk, results in zip(objs_with_pk, returned_columns):
|
|
152
|
-
for result, field in zip(results, opts.db_returning_fields):
|
|
153
|
-
if field != opts.pk:
|
|
154
|
-
setattr(obj_with_pk, field.attname, result)
|
|
155
|
-
for obj_with_pk in objs_with_pk:
|
|
156
|
-
obj_with_pk._state.adding = False
|
|
157
|
-
obj_with_pk._state.db = self.db
|
|
158
|
-
if objs_without_pk:
|
|
159
|
-
fields_wo_pk = [f for f in fields if not isinstance(f, AutoField)]
|
|
160
|
-
returned_columns = self._batched_insert(
|
|
161
|
-
objs_without_pk,
|
|
162
|
-
fields_wo_pk,
|
|
163
|
-
batch_size,
|
|
164
|
-
on_conflict=on_conflict,
|
|
165
|
-
update_fields=update_fields,
|
|
166
|
-
unique_fields=unique_fields,
|
|
167
|
-
)
|
|
168
|
-
connection = connections[self.db]
|
|
169
|
-
if (
|
|
170
|
-
connection.features.can_return_rows_from_bulk_insert
|
|
171
|
-
and on_conflict is None
|
|
172
|
-
):
|
|
173
|
-
assert len(returned_columns) == len(objs_without_pk)
|
|
174
|
-
for obj_without_pk, results in zip(objs_without_pk, returned_columns):
|
|
175
|
-
for result, field in zip(results, opts.db_returning_fields):
|
|
176
|
-
setattr(obj_without_pk, field.attname, result)
|
|
177
|
-
obj_without_pk._state.adding = False
|
|
178
|
-
obj_without_pk._state.db = self.db
|
|
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()
|
|
179
213
|
|
|
180
214
|
if not bypass_hooks:
|
|
181
|
-
engine.run(model_cls,
|
|
215
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
182
216
|
|
|
183
217
|
return objs
|
|
184
218
|
|
|
185
|
-
# --- Private helper methods
|
|
219
|
+
# --- Private helper methods ---
|
|
186
220
|
|
|
187
221
|
def _detect_modified_fields(self, new_instances, original_instances):
|
|
188
222
|
"""
|
|
@@ -240,19 +274,19 @@ class BulkHookManager(models.Manager):
|
|
|
240
274
|
chain.reverse()
|
|
241
275
|
return chain
|
|
242
276
|
|
|
243
|
-
def _mti_bulk_create(self, objs, inheritance_chain, **kwargs):
|
|
277
|
+
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
244
278
|
"""
|
|
245
279
|
Implements workaround: individual saves for parents, bulk create for child.
|
|
246
280
|
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
247
281
|
"""
|
|
282
|
+
if inheritance_chain is None:
|
|
283
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
284
|
+
|
|
248
285
|
batch_size = kwargs.get("batch_size") or len(objs)
|
|
249
286
|
created_objects = []
|
|
250
287
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
251
288
|
for i in range(0, len(objs), batch_size):
|
|
252
289
|
batch = objs[i : i + batch_size]
|
|
253
|
-
# Set auto_now fields for each model in the chain
|
|
254
|
-
for model in inheritance_chain:
|
|
255
|
-
self._set_auto_now_fields(batch, model)
|
|
256
290
|
batch_result = self._process_mti_batch(
|
|
257
291
|
batch, inheritance_chain, **kwargs
|
|
258
292
|
)
|
|
@@ -284,8 +318,6 @@ class BulkHookManager(models.Manager):
|
|
|
284
318
|
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
285
319
|
)
|
|
286
320
|
child_objects.append(child_obj)
|
|
287
|
-
# Handle order_with_respect_to like Django's bulk_create
|
|
288
|
-
self._handle_order_with_respect_to(child_objects)
|
|
289
321
|
# If the child model is still MTI, call our own logic recursively
|
|
290
322
|
if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
|
|
291
323
|
# Build inheritance chain for the child model
|
|
@@ -348,104 +380,3 @@ class BulkHookManager(models.Manager):
|
|
|
348
380
|
if parent_link:
|
|
349
381
|
setattr(child_obj, parent_link.name, parent_instance)
|
|
350
382
|
return child_obj
|
|
351
|
-
|
|
352
|
-
def _set_auto_now_fields(self, objs, model):
|
|
353
|
-
"""
|
|
354
|
-
Set auto_now_add and auto_now fields on objects before bulk_create.
|
|
355
|
-
"""
|
|
356
|
-
from django.utils import timezone
|
|
357
|
-
|
|
358
|
-
now = timezone.now()
|
|
359
|
-
for obj in objs:
|
|
360
|
-
for field in model._meta.local_fields:
|
|
361
|
-
if (
|
|
362
|
-
getattr(field, "auto_now_add", False)
|
|
363
|
-
and getattr(obj, field.name, None) is None
|
|
364
|
-
):
|
|
365
|
-
setattr(obj, field.name, now)
|
|
366
|
-
if getattr(field, "auto_now", False):
|
|
367
|
-
setattr(obj, field.name, now)
|
|
368
|
-
|
|
369
|
-
@transaction.atomic
|
|
370
|
-
def bulk_delete(
|
|
371
|
-
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
372
|
-
):
|
|
373
|
-
if not objs:
|
|
374
|
-
return []
|
|
375
|
-
|
|
376
|
-
model_cls = self.model
|
|
377
|
-
|
|
378
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
379
|
-
raise TypeError(
|
|
380
|
-
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
ctx = HookContext(model_cls)
|
|
384
|
-
|
|
385
|
-
if not bypass_hooks:
|
|
386
|
-
# Run validation hooks first
|
|
387
|
-
if not bypass_validation:
|
|
388
|
-
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
389
|
-
|
|
390
|
-
# Then run business logic hooks
|
|
391
|
-
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
392
|
-
|
|
393
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
394
|
-
|
|
395
|
-
# Use base manager for the actual deletion to prevent recursion
|
|
396
|
-
# The hooks have already been fired above, so we don't need them again
|
|
397
|
-
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
398
|
-
|
|
399
|
-
if not bypass_hooks:
|
|
400
|
-
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
401
|
-
|
|
402
|
-
@transaction.atomic
|
|
403
|
-
def update(self, **kwargs):
|
|
404
|
-
objs = list(self.all())
|
|
405
|
-
if not objs:
|
|
406
|
-
return 0
|
|
407
|
-
for key, value in kwargs.items():
|
|
408
|
-
for obj in objs:
|
|
409
|
-
setattr(obj, key, value)
|
|
410
|
-
self.bulk_update(objs, fields=list(kwargs.keys()))
|
|
411
|
-
return len(objs)
|
|
412
|
-
|
|
413
|
-
@transaction.atomic
|
|
414
|
-
def delete(self):
|
|
415
|
-
objs = list(self.all())
|
|
416
|
-
if not objs:
|
|
417
|
-
return 0
|
|
418
|
-
self.bulk_delete(objs)
|
|
419
|
-
return len(objs)
|
|
420
|
-
|
|
421
|
-
@transaction.atomic
|
|
422
|
-
def save(self, obj):
|
|
423
|
-
if obj.pk:
|
|
424
|
-
self.bulk_update(
|
|
425
|
-
[obj],
|
|
426
|
-
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
427
|
-
)
|
|
428
|
-
else:
|
|
429
|
-
self.bulk_create([obj])
|
|
430
|
-
return obj
|
|
431
|
-
|
|
432
|
-
def _handle_order_with_respect_to(self, objs):
|
|
433
|
-
"""
|
|
434
|
-
Set _order fields for models with order_with_respect_to.
|
|
435
|
-
"""
|
|
436
|
-
for obj in objs:
|
|
437
|
-
order_with_respect_to = obj.__class__._meta.order_with_respect_to
|
|
438
|
-
if order_with_respect_to:
|
|
439
|
-
key = getattr(obj, order_with_respect_to.attname)
|
|
440
|
-
obj._order = key
|
|
441
|
-
# Group by the value of order_with_respect_to
|
|
442
|
-
groups = defaultdict(list)
|
|
443
|
-
for obj in objs:
|
|
444
|
-
order_with_respect_to = obj.__class__._meta.order_with_respect_to
|
|
445
|
-
if order_with_respect_to:
|
|
446
|
-
key = getattr(obj, order_with_respect_to.attname)
|
|
447
|
-
groups[key].append(obj)
|
|
448
|
-
# Enumerate within each group
|
|
449
|
-
for group_objs in groups.values():
|
|
450
|
-
for i, obj in enumerate(group_objs):
|
|
451
|
-
obj._order = i
|
|
@@ -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
|