GeneralManager 0.0.0__py3-none-any.whl
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.
- general_manager/__init__.py +0 -0
- general_manager/api/graphql.py +732 -0
- general_manager/api/mutation.py +143 -0
- general_manager/api/property.py +20 -0
- general_manager/apps.py +83 -0
- general_manager/auxiliary/__init__.py +2 -0
- general_manager/auxiliary/argsToKwargs.py +25 -0
- general_manager/auxiliary/filterParser.py +97 -0
- general_manager/auxiliary/noneToZero.py +12 -0
- general_manager/cache/cacheDecorator.py +72 -0
- general_manager/cache/cacheTracker.py +33 -0
- general_manager/cache/dependencyIndex.py +300 -0
- general_manager/cache/pathMapping.py +151 -0
- general_manager/cache/signals.py +48 -0
- general_manager/factory/__init__.py +5 -0
- general_manager/factory/factories.py +287 -0
- general_manager/factory/lazy_methods.py +38 -0
- general_manager/interface/__init__.py +3 -0
- general_manager/interface/baseInterface.py +308 -0
- general_manager/interface/calculationInterface.py +406 -0
- general_manager/interface/databaseInterface.py +726 -0
- general_manager/manager/__init__.py +3 -0
- general_manager/manager/generalManager.py +136 -0
- general_manager/manager/groupManager.py +288 -0
- general_manager/manager/input.py +48 -0
- general_manager/manager/meta.py +75 -0
- general_manager/measurement/__init__.py +2 -0
- general_manager/measurement/measurement.py +233 -0
- general_manager/measurement/measurementField.py +152 -0
- general_manager/permission/__init__.py +1 -0
- general_manager/permission/basePermission.py +178 -0
- general_manager/permission/fileBasedPermission.py +0 -0
- general_manager/permission/managerBasedPermission.py +171 -0
- general_manager/permission/permissionChecks.py +53 -0
- general_manager/permission/permissionDataManager.py +55 -0
- general_manager/rule/__init__.py +1 -0
- general_manager/rule/handler.py +122 -0
- general_manager/rule/rule.py +313 -0
- generalmanager-0.0.0.dist-info/METADATA +207 -0
- generalmanager-0.0.0.dist-info/RECORD +43 -0
- generalmanager-0.0.0.dist-info/WHEEL +5 -0
- generalmanager-0.0.0.dist-info/licenses/LICENSE +29 -0
- generalmanager-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,300 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import time
|
3
|
+
import ast
|
4
|
+
import re
|
5
|
+
|
6
|
+
from django.core.cache import cache
|
7
|
+
from general_manager.cache.signals import post_data_change, pre_data_change
|
8
|
+
from django.dispatch import receiver
|
9
|
+
from typing import Literal, Any, Iterable, TYPE_CHECKING, Type
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from general_manager.manager.generalManager import GeneralManager
|
13
|
+
|
14
|
+
type general_manager_name = str # e.g. "Project", "Derivative", "User"
|
15
|
+
type attribute = str # e.g. "field", "name", "id"
|
16
|
+
type lookup = str # e.g. "field__gt", "field__in", "field__contains", "field"
|
17
|
+
type cache_keys = set[str] # e.g. "cache_key_1", "cache_key_2"
|
18
|
+
type identifier = str # e.g. "{'id': 1}"", "{'project': Project(**{'id': 1})}", ...
|
19
|
+
type dependency_index = dict[
|
20
|
+
Literal["filter", "exclude"],
|
21
|
+
dict[
|
22
|
+
general_manager_name,
|
23
|
+
dict[attribute, dict[lookup, cache_keys]],
|
24
|
+
],
|
25
|
+
]
|
26
|
+
|
27
|
+
|
28
|
+
# -----------------------------------------------------------------------------
|
29
|
+
# CONFIG
|
30
|
+
# -----------------------------------------------------------------------------
|
31
|
+
INDEX_KEY = "dependency_index" # Key unter dem der gesamte Index liegt
|
32
|
+
LOCK_KEY = "dependency_index_lock" # Key für das Sperr‑Mutex
|
33
|
+
LOCK_TIMEOUT = 5 # Sekunden TTL für den Lock
|
34
|
+
|
35
|
+
|
36
|
+
# -----------------------------------------------------------------------------
|
37
|
+
# LOCKING HELPERS
|
38
|
+
# -----------------------------------------------------------------------------
|
39
|
+
def acquire_lock(timeout: int = LOCK_TIMEOUT) -> bool:
|
40
|
+
"""Atomar: legt den LOCK_KEY an, wenn noch frei."""
|
41
|
+
return cache.add(LOCK_KEY, "1", timeout)
|
42
|
+
|
43
|
+
|
44
|
+
def release_lock() -> None:
|
45
|
+
"""Gibt den Lock frei."""
|
46
|
+
cache.delete(LOCK_KEY)
|
47
|
+
|
48
|
+
|
49
|
+
# -----------------------------------------------------------------------------
|
50
|
+
# INDEX ACCESS
|
51
|
+
# -----------------------------------------------------------------------------
|
52
|
+
def get_full_index() -> dependency_index:
|
53
|
+
"""Lädt oder initialisiert den kompletten Index."""
|
54
|
+
idx = cache.get(INDEX_KEY, None)
|
55
|
+
if idx is None:
|
56
|
+
idx: dependency_index = {"filter": {}, "exclude": {}}
|
57
|
+
cache.set(INDEX_KEY, idx, None)
|
58
|
+
return idx
|
59
|
+
|
60
|
+
|
61
|
+
def set_full_index(idx: dependency_index) -> None:
|
62
|
+
"""Schreibt den kompletten Index zurück in den Cache."""
|
63
|
+
cache.set(INDEX_KEY, idx, None)
|
64
|
+
|
65
|
+
|
66
|
+
# -----------------------------------------------------------------------------
|
67
|
+
# DEPENDENCY RECORDING
|
68
|
+
# -----------------------------------------------------------------------------
|
69
|
+
def record_dependencies(
|
70
|
+
cache_key: str,
|
71
|
+
dependencies: Iterable[
|
72
|
+
tuple[
|
73
|
+
general_manager_name,
|
74
|
+
Literal["filter", "exclude", "identification"],
|
75
|
+
identifier,
|
76
|
+
]
|
77
|
+
],
|
78
|
+
) -> None:
|
79
|
+
"""
|
80
|
+
Speichert die Abhängigkeiten eines Cache Eintrags.
|
81
|
+
:param cache_key: der Key unter dem das Ergebnis im cache steht
|
82
|
+
:param dependencies: Iterable von Tuplen (model_name, action, identifier)
|
83
|
+
action ∈ {'filter','exclude'} oder sonstige → 'id'
|
84
|
+
identifier = für filter/exclude: Dict String,
|
85
|
+
sonst: Primärschlüssel als String
|
86
|
+
"""
|
87
|
+
# 1) Lock holen (Spin‑Lock mit Timeout)
|
88
|
+
start = time.time()
|
89
|
+
while not acquire_lock():
|
90
|
+
if time.time() - start > LOCK_TIMEOUT:
|
91
|
+
raise TimeoutError("Could not aquire lock for record_dependencies")
|
92
|
+
time.sleep(0.05)
|
93
|
+
|
94
|
+
try:
|
95
|
+
idx = get_full_index()
|
96
|
+
for model_name, action, identifier in dependencies:
|
97
|
+
if action in ("filter", "exclude"):
|
98
|
+
params = ast.literal_eval(identifier)
|
99
|
+
section = idx[action].setdefault(model_name, {})
|
100
|
+
for lookup, val in params.items():
|
101
|
+
lookup_map = section.setdefault(lookup, {})
|
102
|
+
val_key = repr(val)
|
103
|
+
lookup_map.setdefault(val_key, set()).add(cache_key)
|
104
|
+
|
105
|
+
else:
|
106
|
+
# Direkter ID‑Lookup als simpler filter auf 'id'
|
107
|
+
section = idx["filter"].setdefault(model_name, {})
|
108
|
+
lookup_map = section.setdefault("identification", {})
|
109
|
+
val_key = identifier
|
110
|
+
lookup_map.setdefault(val_key, set()).add(cache_key)
|
111
|
+
|
112
|
+
set_full_index(idx)
|
113
|
+
|
114
|
+
finally:
|
115
|
+
release_lock()
|
116
|
+
|
117
|
+
|
118
|
+
# -----------------------------------------------------------------------------
|
119
|
+
# INDEX CLEANUP
|
120
|
+
# -----------------------------------------------------------------------------
|
121
|
+
def remove_cache_key_from_index(cache_key: str) -> bool:
|
122
|
+
"""
|
123
|
+
Entfernt einen cache_key aus allen Einträgen in filter/exclude.
|
124
|
+
Nützlich, sobald Du den Cache gelöscht hast.
|
125
|
+
"""
|
126
|
+
start = time.time()
|
127
|
+
while not acquire_lock():
|
128
|
+
if time.time() - start > LOCK_TIMEOUT:
|
129
|
+
return False
|
130
|
+
time.sleep(0.05)
|
131
|
+
|
132
|
+
try:
|
133
|
+
idx = get_full_index()
|
134
|
+
for action in ("filter", "exclude"):
|
135
|
+
action_section = idx.get(action, {})
|
136
|
+
for mname, model_section in list(action_section.items()):
|
137
|
+
for lookup, lookup_map in list(model_section.items()):
|
138
|
+
for val_key, key_set in list(lookup_map.items()):
|
139
|
+
if cache_key in key_set:
|
140
|
+
key_set.remove(cache_key)
|
141
|
+
if not key_set:
|
142
|
+
del lookup_map[val_key]
|
143
|
+
if not lookup_map:
|
144
|
+
del model_section[lookup]
|
145
|
+
if not model_section:
|
146
|
+
del action_section[mname]
|
147
|
+
set_full_index(idx)
|
148
|
+
finally:
|
149
|
+
release_lock()
|
150
|
+
return True
|
151
|
+
|
152
|
+
|
153
|
+
# -----------------------------------------------------------------------------
|
154
|
+
# CACHE INVALIDATION
|
155
|
+
# -----------------------------------------------------------------------------
|
156
|
+
def invalidate_cache_key(cache_key: str) -> None:
|
157
|
+
"""Löscht den CacheEintrag – hier nutzt du deinen CacheBackend."""
|
158
|
+
cache.delete(cache_key)
|
159
|
+
|
160
|
+
|
161
|
+
@receiver(pre_data_change)
|
162
|
+
def capture_old_values(
|
163
|
+
sender: Type[GeneralManager], instance: GeneralManager | None, **kwargs
|
164
|
+
) -> None:
|
165
|
+
if instance is None:
|
166
|
+
# Wenn es kein Modell ist, gibt es nichts zu tun
|
167
|
+
return
|
168
|
+
manager_name = sender.__name__
|
169
|
+
idx = get_full_index()
|
170
|
+
# Welche Lookups interessieren uns für diesen Model?
|
171
|
+
lookups = set()
|
172
|
+
for action in ("filter", "exclude"):
|
173
|
+
lookups |= set(idx.get(action, {}).get(manager_name, {}))
|
174
|
+
if lookups and instance.identification:
|
175
|
+
# Speichere alle relevanten Attribute für später
|
176
|
+
vals = {}
|
177
|
+
for lookup in lookups:
|
178
|
+
attr_path = lookup.split("__")
|
179
|
+
obj = instance
|
180
|
+
for attr in attr_path:
|
181
|
+
obj = getattr(obj, attr, None)
|
182
|
+
if obj is None:
|
183
|
+
break
|
184
|
+
vals[lookup] = obj
|
185
|
+
setattr(instance, "_old_values", vals)
|
186
|
+
|
187
|
+
|
188
|
+
# -----------------------------------------------------------------------------
|
189
|
+
# GENERIC CACHE INVALIDATION: vergleicht alt vs. neu und invalidiert nur bei Übergang
|
190
|
+
# -----------------------------------------------------------------------------
|
191
|
+
|
192
|
+
|
193
|
+
@receiver(post_data_change)
|
194
|
+
def generic_cache_invalidation(
|
195
|
+
sender: type[GeneralManager],
|
196
|
+
instance: GeneralManager,
|
197
|
+
old_relevant_values: dict[str, Any],
|
198
|
+
**kwargs,
|
199
|
+
):
|
200
|
+
manager_name = sender.__name__
|
201
|
+
idx = get_full_index()
|
202
|
+
|
203
|
+
def matches(op: str, value: Any, val_key: Any) -> bool:
|
204
|
+
if value is None:
|
205
|
+
return False
|
206
|
+
|
207
|
+
# eq
|
208
|
+
if op == "eq":
|
209
|
+
return repr(value) == val_key
|
210
|
+
|
211
|
+
# in
|
212
|
+
if op == "in":
|
213
|
+
try:
|
214
|
+
seq = ast.literal_eval(val_key)
|
215
|
+
return value in seq
|
216
|
+
except:
|
217
|
+
return False
|
218
|
+
|
219
|
+
# range
|
220
|
+
if op in ("gt", "gte", "lt", "lte"):
|
221
|
+
try:
|
222
|
+
thr = type(value)(ast.literal_eval(val_key))
|
223
|
+
except:
|
224
|
+
return False
|
225
|
+
if op == "gt":
|
226
|
+
return value > thr
|
227
|
+
if op == "gte":
|
228
|
+
return value >= thr
|
229
|
+
if op == "lt":
|
230
|
+
return value < thr
|
231
|
+
if op == "lte":
|
232
|
+
return value <= thr
|
233
|
+
|
234
|
+
# wildcard / regex
|
235
|
+
if op in ("contains", "startswith", "endswith", "regex"):
|
236
|
+
try:
|
237
|
+
pattern = re.compile(val_key)
|
238
|
+
except:
|
239
|
+
return False
|
240
|
+
text = value or ""
|
241
|
+
return bool(pattern.search(text))
|
242
|
+
|
243
|
+
return False
|
244
|
+
|
245
|
+
for action in ("filter", "exclude"):
|
246
|
+
model_section = idx.get(action, {}).get(manager_name, {})
|
247
|
+
for lookup, lookup_map in model_section.items():
|
248
|
+
# 1) Operator und Attributpfad ermitteln
|
249
|
+
parts = lookup.split("__")
|
250
|
+
if parts[-1] in (
|
251
|
+
"gt",
|
252
|
+
"gte",
|
253
|
+
"lt",
|
254
|
+
"lte",
|
255
|
+
"in",
|
256
|
+
"contains",
|
257
|
+
"startswith",
|
258
|
+
"endswith",
|
259
|
+
"regex",
|
260
|
+
):
|
261
|
+
op = parts[-1]
|
262
|
+
attr_path = parts[:-1]
|
263
|
+
else:
|
264
|
+
op = "eq"
|
265
|
+
attr_path = parts
|
266
|
+
|
267
|
+
# 2) Alten und neuen Wert holen
|
268
|
+
old_val = old_relevant_values.get(lookup)
|
269
|
+
|
270
|
+
obj = instance
|
271
|
+
for attr in attr_path:
|
272
|
+
obj = getattr(obj, attr, None)
|
273
|
+
if obj is None:
|
274
|
+
break
|
275
|
+
new_val = obj
|
276
|
+
|
277
|
+
# 3) Für jedes val_key prüfen
|
278
|
+
for val_key, cache_keys in list(lookup_map.items()):
|
279
|
+
old_match = matches(op, old_val, val_key)
|
280
|
+
new_match = matches(op, new_val, val_key)
|
281
|
+
|
282
|
+
if action == "filter":
|
283
|
+
# Direkte & alle Filter-Abhängigkeiten: immer invalidieren, wenn neu matcht
|
284
|
+
if new_match:
|
285
|
+
print(
|
286
|
+
f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
|
287
|
+
)
|
288
|
+
for ck in list(cache_keys):
|
289
|
+
invalidate_cache_key(ck)
|
290
|
+
remove_cache_key_from_index(ck)
|
291
|
+
|
292
|
+
else: # action == 'exclude'
|
293
|
+
# Excludes: nur invalidieren, wenn sich der Match-Status ändert
|
294
|
+
if old_match != new_match:
|
295
|
+
print(
|
296
|
+
f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
|
297
|
+
)
|
298
|
+
for ck in list(cache_keys):
|
299
|
+
invalidate_cache_key(ck)
|
300
|
+
remove_cache_key_from_index(ck)
|
@@ -0,0 +1,151 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import TYPE_CHECKING, cast, get_args
|
3
|
+
from general_manager.manager.meta import GeneralManagerMeta
|
4
|
+
from general_manager.api.property import GraphQLProperty
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from general_manager.interface.baseInterface import Bucket
|
8
|
+
from general_manager.manager.generalManager import GeneralManager
|
9
|
+
|
10
|
+
|
11
|
+
type PathStart = str
|
12
|
+
type PathDestination = str
|
13
|
+
|
14
|
+
|
15
|
+
class PathMap:
|
16
|
+
|
17
|
+
instance: PathMap
|
18
|
+
mapping: dict[tuple[PathStart, PathDestination], PathTracer] = {}
|
19
|
+
|
20
|
+
def __new__(cls, *args, **kwargs):
|
21
|
+
if not hasattr(cls, "instance"):
|
22
|
+
cls.instance = super().__new__(cls)
|
23
|
+
cls.createPathMapping()
|
24
|
+
return cls.instance
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def createPathMapping(cls):
|
28
|
+
all_managed_classes = GeneralManagerMeta.all_classes
|
29
|
+
for start_class in all_managed_classes:
|
30
|
+
for destination_class in all_managed_classes:
|
31
|
+
if start_class != destination_class:
|
32
|
+
cls.instance.mapping[
|
33
|
+
(start_class.__name__, destination_class.__name__)
|
34
|
+
] = PathTracer(start_class, destination_class)
|
35
|
+
|
36
|
+
def __init__(self, path_start: PathStart | GeneralManager | type[GeneralManager]):
|
37
|
+
from general_manager.manager.generalManager import GeneralManager
|
38
|
+
|
39
|
+
if isinstance(path_start, GeneralManager):
|
40
|
+
self.start_instance = path_start
|
41
|
+
self.start_class = path_start.__class__
|
42
|
+
self.start_class_name = path_start.__class__.__name__
|
43
|
+
elif isinstance(path_start, type):
|
44
|
+
self.start_instance = None
|
45
|
+
self.start_class = path_start
|
46
|
+
self.start_class_name = path_start.__name__
|
47
|
+
else:
|
48
|
+
self.start_instance = None
|
49
|
+
self.start_class = None
|
50
|
+
self.start_class_name = path_start
|
51
|
+
|
52
|
+
def to(
|
53
|
+
self, path_destination: PathDestination | type[GeneralManager] | str
|
54
|
+
) -> PathTracer | None:
|
55
|
+
if isinstance(path_destination, type):
|
56
|
+
path_destination = path_destination.__name__
|
57
|
+
|
58
|
+
tracer = self.mapping.get((self.start_class_name, path_destination), None)
|
59
|
+
if not tracer:
|
60
|
+
return None
|
61
|
+
return tracer
|
62
|
+
|
63
|
+
def goTo(
|
64
|
+
self, path_destination: PathDestination | type[GeneralManager] | str
|
65
|
+
) -> GeneralManager | Bucket | None:
|
66
|
+
if isinstance(path_destination, type):
|
67
|
+
path_destination = path_destination.__name__
|
68
|
+
|
69
|
+
tracer = self.mapping.get((self.start_class_name, path_destination), None)
|
70
|
+
if not tracer:
|
71
|
+
return None
|
72
|
+
if not self.start_instance:
|
73
|
+
raise ValueError("Cannot call goTo on a PathMap without a start instance.")
|
74
|
+
return tracer.traversePath(self.start_instance)
|
75
|
+
|
76
|
+
def getAllConnected(self) -> set[str]:
|
77
|
+
"""
|
78
|
+
Returns a list of all classes that are connected to the start class.
|
79
|
+
"""
|
80
|
+
connected_classes: set[str] = set()
|
81
|
+
for path_tuple in self.mapping.keys():
|
82
|
+
if path_tuple[0] == self.start_class_name:
|
83
|
+
destination_class_name = path_tuple[1]
|
84
|
+
connected_classes.add(destination_class_name)
|
85
|
+
return connected_classes
|
86
|
+
|
87
|
+
|
88
|
+
class PathTracer:
|
89
|
+
def __init__(
|
90
|
+
self, start_class: type[GeneralManager], destination_class: type[GeneralManager]
|
91
|
+
):
|
92
|
+
self.start_class = start_class
|
93
|
+
self.destination_class = destination_class
|
94
|
+
if self.start_class == self.destination_class:
|
95
|
+
self.path = []
|
96
|
+
else:
|
97
|
+
self.path = self.createPath(start_class, [])
|
98
|
+
|
99
|
+
def createPath(
|
100
|
+
self, current_manager: type[GeneralManager], path: list[str]
|
101
|
+
) -> list[str] | None:
|
102
|
+
from general_manager.manager.generalManager import GeneralManager
|
103
|
+
|
104
|
+
current_connections = {
|
105
|
+
attr_name: attr_value["type"]
|
106
|
+
for attr_name, attr_value in current_manager.Interface.getAttributeTypes().items()
|
107
|
+
}
|
108
|
+
for attr_name, attr_value in current_manager.__dict__.items():
|
109
|
+
if not isinstance(attr_value, GraphQLProperty):
|
110
|
+
continue
|
111
|
+
type_hints = get_args(attr_value.graphql_type_hint)
|
112
|
+
field_type = (
|
113
|
+
type_hints[0]
|
114
|
+
if type_hints
|
115
|
+
else cast(type, attr_value.graphql_type_hint)
|
116
|
+
)
|
117
|
+
current_connections[attr_name] = field_type
|
118
|
+
for attr, attr_type in current_connections.items():
|
119
|
+
if attr in path or attr_type == self.start_class:
|
120
|
+
continue
|
121
|
+
if not issubclass(attr_type, GeneralManager):
|
122
|
+
continue
|
123
|
+
if attr_type == self.destination_class:
|
124
|
+
return [*path, attr]
|
125
|
+
result = self.createPath(attr_type, [*path, attr])
|
126
|
+
if result:
|
127
|
+
return result
|
128
|
+
|
129
|
+
return None
|
130
|
+
|
131
|
+
def traversePath(
|
132
|
+
self, start_instance: GeneralManager | Bucket
|
133
|
+
) -> GeneralManager | Bucket | None:
|
134
|
+
from general_manager.interface.baseInterface import Bucket
|
135
|
+
|
136
|
+
current_instance = start_instance
|
137
|
+
if not self.path:
|
138
|
+
return None
|
139
|
+
for attr in self.path:
|
140
|
+
if not isinstance(current_instance, Bucket):
|
141
|
+
current_instance = getattr(current_instance, attr)
|
142
|
+
continue
|
143
|
+
new_instance = None
|
144
|
+
for entry in current_instance:
|
145
|
+
if not new_instance:
|
146
|
+
new_instance = getattr(entry, attr)
|
147
|
+
else:
|
148
|
+
new_instance = new_instance | getattr(entry, attr)
|
149
|
+
current_instance = new_instance
|
150
|
+
|
151
|
+
return current_instance
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from django.dispatch import Signal
|
2
|
+
from typing import Callable, Any
|
3
|
+
from functools import wraps
|
4
|
+
|
5
|
+
post_data_change = Signal()
|
6
|
+
|
7
|
+
pre_data_change = Signal()
|
8
|
+
|
9
|
+
|
10
|
+
def dataChange(func: Callable[..., Any]) -> Callable:
|
11
|
+
"""
|
12
|
+
Signal to indicate that data has changed.
|
13
|
+
"""
|
14
|
+
|
15
|
+
@wraps(func)
|
16
|
+
def wrapper(*args, **kwargs):
|
17
|
+
action = func.__name__
|
18
|
+
if func.__name__ == "create":
|
19
|
+
sender = args[0]
|
20
|
+
instance_before = None
|
21
|
+
else:
|
22
|
+
instance = args[0]
|
23
|
+
sender = instance.__class__
|
24
|
+
instance_before = instance
|
25
|
+
pre_data_change.send(
|
26
|
+
sender=sender,
|
27
|
+
instance=instance_before,
|
28
|
+
action=action,
|
29
|
+
**kwargs,
|
30
|
+
)
|
31
|
+
old_relevant_values = getattr(instance_before, "_old_values", {})
|
32
|
+
result = (
|
33
|
+
func.__func__(*args, **kwargs)
|
34
|
+
if isinstance(func, classmethod)
|
35
|
+
else func(*args, **kwargs)
|
36
|
+
)
|
37
|
+
instance = result
|
38
|
+
|
39
|
+
post_data_change.send(
|
40
|
+
sender=sender,
|
41
|
+
instance=instance,
|
42
|
+
action=action,
|
43
|
+
old_relevant_values=old_relevant_values,
|
44
|
+
**kwargs,
|
45
|
+
)
|
46
|
+
return result
|
47
|
+
|
48
|
+
return wrapper
|