GeneralManager 0.14.0__py3-none-any.whl → 0.15.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 +49 -0
- general_manager/api/__init__.py +36 -0
- general_manager/api/graphql.py +92 -43
- general_manager/api/mutation.py +35 -10
- general_manager/api/property.py +26 -3
- general_manager/apps.py +23 -16
- general_manager/bucket/__init__.py +32 -0
- general_manager/bucket/baseBucket.py +76 -64
- general_manager/bucket/calculationBucket.py +188 -108
- general_manager/bucket/databaseBucket.py +130 -49
- general_manager/bucket/groupBucket.py +113 -60
- general_manager/cache/__init__.py +38 -0
- general_manager/cache/cacheDecorator.py +29 -17
- general_manager/cache/cacheTracker.py +34 -15
- general_manager/cache/dependencyIndex.py +117 -33
- general_manager/cache/modelDependencyCollector.py +17 -8
- general_manager/cache/signals.py +17 -6
- general_manager/factory/__init__.py +34 -5
- general_manager/factory/autoFactory.py +57 -60
- general_manager/factory/factories.py +39 -14
- general_manager/factory/factoryMethods.py +38 -1
- general_manager/interface/__init__.py +36 -0
- general_manager/interface/baseInterface.py +71 -27
- general_manager/interface/calculationInterface.py +18 -10
- general_manager/interface/databaseBasedInterface.py +102 -71
- general_manager/interface/databaseInterface.py +66 -20
- general_manager/interface/models.py +10 -4
- general_manager/interface/readOnlyInterface.py +44 -30
- general_manager/manager/__init__.py +36 -3
- general_manager/manager/generalManager.py +73 -47
- general_manager/manager/groupManager.py +72 -17
- general_manager/manager/input.py +23 -15
- general_manager/manager/meta.py +53 -53
- general_manager/measurement/__init__.py +37 -2
- general_manager/measurement/measurement.py +135 -58
- general_manager/measurement/measurementField.py +161 -61
- general_manager/permission/__init__.py +32 -1
- general_manager/permission/basePermission.py +29 -12
- general_manager/permission/managerBasedPermission.py +32 -26
- general_manager/permission/mutationPermission.py +32 -3
- general_manager/permission/permissionChecks.py +9 -1
- general_manager/permission/permissionDataManager.py +49 -15
- general_manager/permission/utils.py +14 -3
- general_manager/rule/__init__.py +27 -1
- general_manager/rule/handler.py +90 -5
- general_manager/rule/rule.py +40 -27
- general_manager/utils/__init__.py +44 -2
- general_manager/utils/argsToKwargs.py +17 -9
- general_manager/utils/filterParser.py +29 -30
- general_manager/utils/formatString.py +2 -0
- general_manager/utils/jsonEncoder.py +14 -1
- general_manager/utils/makeCacheKey.py +18 -12
- general_manager/utils/noneToZero.py +8 -6
- general_manager/utils/pathMapping.py +92 -29
- general_manager/utils/public_api.py +49 -0
- general_manager/utils/testing.py +135 -69
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/METADATA +38 -4
- generalmanager-0.15.0.dist-info/RECORD +62 -0
- generalmanager-0.15.0.dist-info/licenses/LICENSE +21 -0
- generalmanager-0.14.0.dist-info/RECORD +0 -58
- generalmanager-0.14.0.dist-info/licenses/LICENSE +0 -29
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Custom Django model field storing values as unit-aware measurements."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
|
3
5
|
from django.db import models
|
@@ -6,50 +8,96 @@ from django.db.models.expressions import Col
|
|
6
8
|
from decimal import Decimal
|
7
9
|
import pint
|
8
10
|
from general_manager.measurement.measurement import Measurement, ureg, currency_units
|
11
|
+
from django.db.backends.base.base import BaseDatabaseWrapper
|
12
|
+
from django.db.models import Lookup, Transform
|
13
|
+
from typing import Any, ClassVar, cast
|
9
14
|
|
10
15
|
|
11
16
|
class MeasurementField(models.Field):
|
12
17
|
description = "Stores a measurement (value + unit) but exposes a single field API"
|
13
18
|
|
14
|
-
empty_values = (None, "", [], (), {})
|
19
|
+
empty_values: ClassVar[tuple[object, ...]] = (None, "", [], (), {})
|
15
20
|
|
16
21
|
def __init__(
|
17
|
-
self,
|
18
|
-
|
22
|
+
self,
|
23
|
+
base_unit: str,
|
24
|
+
*args: object,
|
25
|
+
null: bool = False,
|
26
|
+
blank: bool = False,
|
27
|
+
editable: bool = True,
|
28
|
+
**kwargs: object,
|
29
|
+
) -> None:
|
19
30
|
"""
|
20
|
-
|
31
|
+
Configure a measurement field backed by separate value and unit columns.
|
21
32
|
|
22
33
|
Parameters:
|
23
|
-
base_unit (str):
|
24
|
-
|
25
|
-
|
26
|
-
|
34
|
+
base_unit (str): Canonical unit used when normalising stored measurements.
|
35
|
+
args (tuple): Positional arguments forwarded to the base `Field` implementation.
|
36
|
+
null (bool): If True, the measurement may be stored as NULL.
|
37
|
+
blank (bool): If True, forms may submit an empty value.
|
38
|
+
editable (bool): If False, assignments through the API raise a validation error.
|
39
|
+
kwargs (dict): Additional keyword arguments forwarded to the base `Field`.
|
27
40
|
|
28
|
-
|
41
|
+
Returns:
|
42
|
+
None
|
29
43
|
"""
|
30
44
|
self.base_unit = base_unit
|
31
45
|
self.base_dimension = ureg.parse_expression(self.base_unit).dimensionality
|
32
46
|
|
33
|
-
nb = {}
|
34
|
-
if null:
|
35
|
-
nb["null"] = True
|
36
|
-
if blank:
|
37
|
-
nb["blank"] = True
|
38
|
-
|
39
47
|
self.editable = editable
|
40
|
-
self.value_field
|
41
|
-
|
42
|
-
|
43
|
-
|
48
|
+
self.value_field: models.DecimalField[Any]
|
49
|
+
self.unit_field: models.CharField[Any]
|
50
|
+
if null:
|
51
|
+
self.value_field = models.DecimalField(
|
52
|
+
max_digits=30,
|
53
|
+
decimal_places=10,
|
54
|
+
db_index=True,
|
55
|
+
editable=editable,
|
56
|
+
null=True,
|
57
|
+
blank=blank,
|
58
|
+
)
|
59
|
+
self.unit_field = models.CharField(
|
60
|
+
max_length=30,
|
61
|
+
editable=editable,
|
62
|
+
null=True,
|
63
|
+
blank=blank,
|
64
|
+
)
|
65
|
+
else:
|
66
|
+
self.value_field = models.DecimalField(
|
67
|
+
max_digits=30,
|
68
|
+
decimal_places=10,
|
69
|
+
db_index=True,
|
70
|
+
editable=editable,
|
71
|
+
null=False,
|
72
|
+
blank=blank,
|
73
|
+
)
|
74
|
+
self.unit_field = models.CharField(
|
75
|
+
max_length=30,
|
76
|
+
editable=editable,
|
77
|
+
null=False,
|
78
|
+
blank=blank,
|
79
|
+
)
|
44
80
|
|
45
81
|
super().__init__(*args, null=null, blank=blank, editable=editable, **kwargs)
|
46
82
|
|
47
|
-
def contribute_to_class(
|
48
|
-
|
83
|
+
def contribute_to_class(
|
84
|
+
self,
|
85
|
+
cls: type[models.Model],
|
86
|
+
name: str,
|
87
|
+
private_only: bool = False,
|
88
|
+
**kwargs: object,
|
89
|
+
) -> None:
|
49
90
|
"""
|
50
|
-
|
91
|
+
Attach the measurement field and its backing columns to a Django model.
|
92
|
+
|
93
|
+
Parameters:
|
94
|
+
cls (type[models.Model]): Model class receiving the field.
|
95
|
+
name (str): Attribute name of the field on the model.
|
96
|
+
private_only (bool): Whether the field should be treated as private.
|
97
|
+
kwargs (dict): Additional options forwarded to the base implementation.
|
51
98
|
|
52
|
-
|
99
|
+
Returns:
|
100
|
+
None
|
53
101
|
"""
|
54
102
|
super().contribute_to_class(cls, name, private_only=private_only, **kwargs)
|
55
103
|
self.concrete = False
|
@@ -76,47 +124,72 @@ class MeasurementField(models.Field):
|
|
76
124
|
setattr(cls, name, self)
|
77
125
|
|
78
126
|
# ---- ORM Delegation ----
|
79
|
-
def get_col(
|
127
|
+
def get_col(
|
128
|
+
self,
|
129
|
+
alias: str,
|
130
|
+
output_field: models.Field[object, object] | None = None,
|
131
|
+
) -> Col:
|
80
132
|
"""
|
81
|
-
|
133
|
+
Produce a column expression referencing the underlying value field.
|
134
|
+
|
135
|
+
Parameters:
|
136
|
+
alias (str): Table alias used within the query.
|
137
|
+
output_field (models.Field | None): Optional output field override.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Col: ORM expression targeting the numeric component.
|
82
141
|
"""
|
83
142
|
return Col(alias, self.value_field, output_field or self.value_field) # type: ignore
|
84
143
|
|
85
|
-
def get_lookup(self, lookup_name):
|
144
|
+
def get_lookup(self, lookup_name: str) -> type[Lookup]:
|
86
145
|
"""
|
87
|
-
|
146
|
+
Retrieve a lookup class from the underlying decimal field.
|
88
147
|
|
89
148
|
Parameters:
|
90
|
-
|
149
|
+
lookup_name (str): Name of the lookup to resolve.
|
91
150
|
|
92
151
|
Returns:
|
93
|
-
|
152
|
+
type[models.Lookup]: Lookup class implementing the requested comparison.
|
94
153
|
"""
|
95
|
-
return self.value_field.get_lookup(lookup_name)
|
154
|
+
return cast(type[Lookup], self.value_field.get_lookup(lookup_name))
|
96
155
|
|
97
|
-
def get_transform(
|
156
|
+
def get_transform(
|
157
|
+
self,
|
158
|
+
lookup_name: str,
|
159
|
+
) -> type[Transform] | None:
|
98
160
|
"""
|
99
|
-
|
161
|
+
Return a transform callable provided by the underlying decimal field.
|
162
|
+
|
163
|
+
Parameters:
|
164
|
+
lookup_name (str): Name of the transform to resolve.
|
100
165
|
|
101
166
|
Returns:
|
102
|
-
|
167
|
+
models.Transform | None: Transform class when available; otherwise None.
|
103
168
|
"""
|
104
|
-
|
169
|
+
transform = self.value_field.get_transform(lookup_name)
|
170
|
+
return cast(type[Transform] | None, transform)
|
105
171
|
|
106
|
-
def db_type(self, connection) -> None: # type: ignore
|
172
|
+
def db_type(self, connection: BaseDatabaseWrapper) -> None: # type: ignore[override]
|
107
173
|
"""
|
108
|
-
|
174
|
+
Signal to Django that the field does not map to a single column.
|
175
|
+
|
176
|
+
Parameters:
|
177
|
+
connection (BaseDatabaseWrapper): Database connection used for schema generation.
|
109
178
|
|
110
|
-
|
179
|
+
Returns:
|
180
|
+
None
|
111
181
|
"""
|
112
182
|
return None
|
113
183
|
|
114
184
|
def run_validators(self, value: Measurement | None) -> None:
|
115
185
|
"""
|
116
|
-
|
186
|
+
Execute all configured validators when a measurement is provided.
|
117
187
|
|
118
188
|
Parameters:
|
119
|
-
value (Measurement | None):
|
189
|
+
value (Measurement | None): Measurement instance that should satisfy field validators.
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
None
|
120
193
|
"""
|
121
194
|
if value is None:
|
122
195
|
return
|
@@ -127,40 +200,46 @@ class MeasurementField(models.Field):
|
|
127
200
|
self, value: Measurement | None, model_instance: models.Model | None = None
|
128
201
|
) -> Measurement | None:
|
129
202
|
"""
|
130
|
-
|
131
|
-
|
132
|
-
Runs field-level validation and all configured validators on the provided value, returning it unchanged if valid.
|
203
|
+
Validate a measurement value before it is saved to the model.
|
133
204
|
|
134
205
|
Parameters:
|
135
|
-
value (Measurement | None):
|
136
|
-
model_instance (models.Model | None):
|
206
|
+
value (Measurement | None): Measurement provided by forms or assignment.
|
207
|
+
model_instance (models.Model | None): Instance associated with the field, when available.
|
137
208
|
|
138
209
|
Returns:
|
139
210
|
Measurement | None: The validated measurement value, or None if the input was None.
|
211
|
+
|
212
|
+
Raises:
|
213
|
+
ValidationError: If validation fails due to null/blank constraints or validator errors.
|
140
214
|
"""
|
141
215
|
self.validate(value, model_instance)
|
142
216
|
self.run_validators(value)
|
143
217
|
return value
|
144
218
|
|
145
|
-
def to_python(self, value):
|
219
|
+
def to_python(self, value: Measurement | str | None) -> Measurement | str | None:
|
146
220
|
"""
|
147
|
-
|
221
|
+
Convert database values back into Python objects.
|
222
|
+
|
223
|
+
Parameters:
|
224
|
+
value (Any): Value retrieved from the database.
|
148
225
|
|
149
|
-
|
226
|
+
Returns:
|
227
|
+
Any: Original value without modification.
|
150
228
|
"""
|
151
229
|
return value
|
152
230
|
|
153
|
-
def get_prep_value(self, value):
|
231
|
+
def get_prep_value(self, value: Measurement | str | None) -> Decimal | None:
|
154
232
|
"""
|
155
|
-
|
233
|
+
Serialise a measurement for storage by converting it to the base unit magnitude.
|
156
234
|
|
157
|
-
|
235
|
+
Parameters:
|
236
|
+
value (Measurement | str | None): Value provided by the model or form.
|
158
237
|
|
159
238
|
Returns:
|
160
|
-
Decimal
|
239
|
+
Decimal | None: Decimal magnitude in the base unit, or None when no value is supplied.
|
161
240
|
|
162
241
|
Raises:
|
163
|
-
ValidationError: If the value
|
242
|
+
ValidationError: If the value cannot be interpreted as a compatible measurement.
|
164
243
|
"""
|
165
244
|
if value is None:
|
166
245
|
return None
|
@@ -182,12 +261,14 @@ class MeasurementField(models.Field):
|
|
182
261
|
self, instance: models.Model | None, owner: None = None
|
183
262
|
) -> MeasurementField | Measurement | None:
|
184
263
|
"""
|
185
|
-
|
264
|
+
Resolve the field value on an instance, reconstructing the measurement when possible.
|
265
|
+
|
266
|
+
Parameters:
|
267
|
+
instance (models.Model | None): Model instance owning the field, or None when accessed on the class.
|
268
|
+
owner (type[models.Model] | None): Model class owning the descriptor.
|
186
269
|
|
187
270
|
Returns:
|
188
|
-
Measurement:
|
189
|
-
None: If either the value or unit is missing.
|
190
|
-
MeasurementField: If accessed from the class rather than an instance.
|
271
|
+
MeasurementField | Measurement | None: Descriptor when accessed on the class, reconstructed measurement for instances, or None when incomplete.
|
191
272
|
"""
|
192
273
|
if instance is None:
|
193
274
|
return self
|
@@ -202,11 +283,23 @@ class MeasurementField(models.Field):
|
|
202
283
|
qty_orig = qty_base
|
203
284
|
return Measurement(qty_orig.magnitude, str(qty_orig.units))
|
204
285
|
|
205
|
-
def __set__(
|
286
|
+
def __set__(
|
287
|
+
self,
|
288
|
+
instance: models.Model,
|
289
|
+
value: Measurement | str | None,
|
290
|
+
) -> None:
|
206
291
|
"""
|
207
|
-
|
292
|
+
Assign a measurement to the model instance after validating compatibility.
|
293
|
+
|
294
|
+
Parameters:
|
295
|
+
instance (models.Model): Model instance receiving the value.
|
296
|
+
value (Measurement | str | None): Measurement value supplied by the caller.
|
297
|
+
|
298
|
+
Returns:
|
299
|
+
None
|
208
300
|
|
209
|
-
|
301
|
+
Raises:
|
302
|
+
ValidationError: If the field is not editable, the value is invalid, or the units are incompatible.
|
210
303
|
"""
|
211
304
|
if not self.editable:
|
212
305
|
raise ValidationError(f"{self.name} is not editable.")
|
@@ -253,10 +346,17 @@ class MeasurementField(models.Field):
|
|
253
346
|
self, value: Measurement | None, model_instance: models.Model | None = None
|
254
347
|
) -> None:
|
255
348
|
"""
|
256
|
-
|
349
|
+
Enforce null/blank constraints and run validators on the provided value.
|
350
|
+
|
351
|
+
Parameters:
|
352
|
+
value (Measurement | None): Measurement value under validation.
|
353
|
+
model_instance (models.Model | None): Instance owning the field; unused but provided for API compatibility.
|
354
|
+
|
355
|
+
Returns:
|
356
|
+
None
|
257
357
|
|
258
358
|
Raises:
|
259
|
-
ValidationError: If the value
|
359
|
+
ValidationError: If the value violates constraint or validator requirements.
|
260
360
|
"""
|
261
361
|
if value is None:
|
262
362
|
if not self.null:
|
@@ -1 +1,32 @@
|
|
1
|
-
|
1
|
+
"""Permission helpers for GeneralManager mutations and actions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from general_manager.utils.public_api import build_module_dir, resolve_export
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"BasePermission",
|
11
|
+
"ManagerBasedPermission",
|
12
|
+
"MutationPermission",
|
13
|
+
]
|
14
|
+
|
15
|
+
_MODULE_MAP = {
|
16
|
+
"BasePermission": ("general_manager.permission.basePermission", "BasePermission"),
|
17
|
+
"ManagerBasedPermission": ("general_manager.permission.managerBasedPermission", "ManagerBasedPermission"),
|
18
|
+
"MutationPermission": ("general_manager.permission.mutationPermission", "MutationPermission"),
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
def __getattr__(name: str) -> Any:
|
23
|
+
return resolve_export(
|
24
|
+
name,
|
25
|
+
module_all=__all__,
|
26
|
+
module_map=_MODULE_MAP,
|
27
|
+
module_globals=globals(),
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
def __dir__() -> list[str]:
|
32
|
+
return build_module_dir(module_all=__all__, module_globals=globals())
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Base permission contract used by GeneralManager instances."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
from abc import ABC, abstractmethod
|
3
5
|
from typing import TYPE_CHECKING, Any, Literal
|
@@ -16,21 +18,25 @@ if TYPE_CHECKING:
|
|
16
18
|
|
17
19
|
|
18
20
|
class BasePermission(ABC):
|
21
|
+
"""Abstract base class defining CRUD permission checks for managers."""
|
19
22
|
|
20
23
|
def __init__(
|
21
24
|
self,
|
22
25
|
instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
|
23
26
|
request_user: AbstractUser | AnonymousUser,
|
24
27
|
) -> None:
|
28
|
+
"""Initialise the permission context for a specific manager and user."""
|
25
29
|
self._instance = instance
|
26
30
|
self._request_user = request_user
|
27
31
|
|
28
32
|
@property
|
29
33
|
def instance(self) -> PermissionDataManager | GeneralManager | GeneralManagerMeta:
|
34
|
+
"""Return the object against which permission checks are performed."""
|
30
35
|
return self._instance
|
31
36
|
|
32
37
|
@property
|
33
38
|
def request_user(self) -> AbstractUser | AnonymousUser:
|
39
|
+
"""Return the user being evaluated for permission checks."""
|
34
40
|
return self._request_user
|
35
41
|
|
36
42
|
@classmethod
|
@@ -40,6 +46,7 @@ class BasePermission(ABC):
|
|
40
46
|
manager: type[GeneralManager],
|
41
47
|
request_user: AbstractUser | AnonymousUser | Any,
|
42
48
|
) -> None:
|
49
|
+
"""Validate create permissions for the supplied payload."""
|
43
50
|
request_user = cls.getUserWithId(request_user)
|
44
51
|
errors = []
|
45
52
|
permission_data = PermissionDataManager(permission_data=data, manager=manager)
|
@@ -62,6 +69,7 @@ class BasePermission(ABC):
|
|
62
69
|
old_manager_instance: GeneralManager,
|
63
70
|
request_user: AbstractUser | AnonymousUser | Any,
|
64
71
|
) -> None:
|
72
|
+
"""Validate update permissions for the supplied payload."""
|
65
73
|
request_user = cls.getUserWithId(request_user)
|
66
74
|
|
67
75
|
errors = []
|
@@ -86,6 +94,7 @@ class BasePermission(ABC):
|
|
86
94
|
manager_instance: GeneralManager,
|
87
95
|
request_user: AbstractUser | AnonymousUser | Any,
|
88
96
|
) -> None:
|
97
|
+
"""Validate delete permissions for the supplied manager instance."""
|
89
98
|
request_user = cls.getUserWithId(request_user)
|
90
99
|
|
91
100
|
errors = []
|
@@ -106,9 +115,7 @@ class BasePermission(ABC):
|
|
106
115
|
def getUserWithId(
|
107
116
|
user: Any | AbstractUser | AnonymousUser,
|
108
117
|
) -> AbstractUser | AnonymousUser:
|
109
|
-
"""
|
110
|
-
Returns the user with the given id
|
111
|
-
"""
|
118
|
+
"""Return a ``User`` instance given a primary key or user object."""
|
112
119
|
from django.contrib.auth.models import User
|
113
120
|
|
114
121
|
if isinstance(user, (AbstractUser, AnonymousUser)):
|
@@ -124,22 +131,28 @@ class BasePermission(ABC):
|
|
124
131
|
action: Literal["create", "read", "update", "delete"],
|
125
132
|
attriubte: str,
|
126
133
|
) -> bool:
|
134
|
+
"""
|
135
|
+
Determine whether the given action is permitted on the specified attribute.
|
136
|
+
|
137
|
+
Parameters:
|
138
|
+
action (Literal["create", "read", "update", "delete"]): Operation being checked.
|
139
|
+
attriubte (str): Attribute name subject to the permission check.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
bool: True when the action is allowed.
|
143
|
+
"""
|
127
144
|
raise NotImplementedError
|
128
145
|
|
129
146
|
def getPermissionFilter(
|
130
147
|
self,
|
131
148
|
) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
|
132
|
-
"""
|
133
|
-
Returns the filter for the permission
|
134
|
-
"""
|
149
|
+
"""Return the filter/exclude constraints associated with this permission."""
|
135
150
|
raise NotImplementedError
|
136
151
|
|
137
152
|
def _getPermissionFilter(
|
138
153
|
self, permission: str
|
139
154
|
) -> dict[Literal["filter", "exclude"], dict[str, str]]:
|
140
|
-
"""
|
141
|
-
Returns the filter for the permission
|
142
|
-
"""
|
155
|
+
"""Resolve a filter definition for the given permission string."""
|
143
156
|
permission_function, *config = permission.split(":")
|
144
157
|
if permission_function not in permission_functions:
|
145
158
|
raise ValueError(f"Permission {permission} not found")
|
@@ -155,8 +168,12 @@ class BasePermission(ABC):
|
|
155
168
|
permission: str,
|
156
169
|
) -> bool:
|
157
170
|
"""
|
158
|
-
|
159
|
-
|
160
|
-
|
171
|
+
Validate complex permission expressions joined by ``&`` operators.
|
172
|
+
|
173
|
+
Parameters:
|
174
|
+
permission (str): Permission expression (for example, ``isAuthenticated&isMatchingKeyAccount``).
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
bool: True when every sub-permission evaluates to True for the current user.
|
161
178
|
"""
|
162
179
|
return validatePermissionString(permission, self.instance, self.request_user)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Default permission implementation leveraging manager configuration."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
from typing import TYPE_CHECKING, Literal, Optional, Dict
|
3
5
|
from general_manager.permission.basePermission import BasePermission
|
@@ -22,6 +24,8 @@ class notExistent:
|
|
22
24
|
|
23
25
|
|
24
26
|
class ManagerBasedPermission(BasePermission):
|
27
|
+
"""Permission implementation driven by class-level configuration lists."""
|
28
|
+
|
25
29
|
__based_on__: Optional[str] = None
|
26
30
|
__read__: list[str]
|
27
31
|
__create__: list[str]
|
@@ -34,9 +38,11 @@ class ManagerBasedPermission(BasePermission):
|
|
34
38
|
request_user: AbstractUser,
|
35
39
|
) -> None:
|
36
40
|
"""
|
37
|
-
|
38
|
-
|
39
|
-
|
41
|
+
Initialise the permission object and gather default and attribute-level rules.
|
42
|
+
|
43
|
+
Parameters:
|
44
|
+
instance (PermissionDataManager | GeneralManager): Target data used for permission evaluation.
|
45
|
+
request_user (AbstractUser): User whose permissions are being checked.
|
40
46
|
"""
|
41
47
|
super().__init__(instance, request_user)
|
42
48
|
self.__setPermissions()
|
@@ -51,12 +57,7 @@ class ManagerBasedPermission(BasePermission):
|
|
51
57
|
}
|
52
58
|
|
53
59
|
def __setPermissions(self, skip_based_on: bool = False) -> None:
|
54
|
-
|
55
|
-
"""
|
56
|
-
Assigns default permission lists for CRUD actions based on the presence of a related permission attribute.
|
57
|
-
|
58
|
-
If the permission is based on another attribute and `skip_based_on` is False, all default permissions are set to empty lists. Otherwise, read permissions default to `["public"]` and write permissions to `["isAuthenticated"]`. Class-level overrides are respected if present.
|
59
|
-
"""
|
60
|
+
"""Populate CRUD permissions using class-level defaults and overrides."""
|
60
61
|
default_read = ["public"]
|
61
62
|
default_write = ["isAuthenticated"]
|
62
63
|
|
@@ -71,14 +72,14 @@ class ManagerBasedPermission(BasePermission):
|
|
71
72
|
|
72
73
|
def __getBasedOnPermission(self) -> Optional[BasePermission]:
|
73
74
|
"""
|
74
|
-
|
75
|
-
|
75
|
+
Retrieve the permission object referenced by ``__based_on__`` when configured.
|
76
|
+
|
76
77
|
Returns:
|
77
|
-
|
78
|
-
|
78
|
+
BasePermission | None: Permission instance for the related object, if applicable.
|
79
|
+
|
79
80
|
Raises:
|
80
|
-
ValueError: If the
|
81
|
-
TypeError: If the
|
81
|
+
ValueError: If the configured attribute does not exist on the instance.
|
82
|
+
TypeError: If the attribute does not resolve to a `GeneralManager`.
|
82
83
|
"""
|
83
84
|
from general_manager.manager.generalManager import GeneralManager
|
84
85
|
|
@@ -115,6 +116,7 @@ class ManagerBasedPermission(BasePermission):
|
|
115
116
|
def __getAttributePermissions(
|
116
117
|
self,
|
117
118
|
) -> dict[str, dict[permission_type, list[str]]]:
|
119
|
+
"""Collect attribute-level permission overrides defined on the class."""
|
118
120
|
attribute_permissions = {}
|
119
121
|
for attribute in self.__class__.__dict__:
|
120
122
|
if not attribute.startswith("__"):
|
@@ -126,6 +128,16 @@ class ManagerBasedPermission(BasePermission):
|
|
126
128
|
action: permission_type,
|
127
129
|
attriubte: str,
|
128
130
|
) -> bool:
|
131
|
+
"""
|
132
|
+
Determine whether the user has permission to perform ``action`` on ``attribute``.
|
133
|
+
|
134
|
+
Parameters:
|
135
|
+
action (permission_type): CRUD operation being evaluated.
|
136
|
+
attriubte (str): Attribute name subject to the permission check.
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
bool: True when the action is permitted.
|
140
|
+
"""
|
129
141
|
if (
|
130
142
|
self.__based_on_permission
|
131
143
|
and not self.__based_on_permission.checkPermission(action, attriubte)
|
@@ -166,11 +178,7 @@ class ManagerBasedPermission(BasePermission):
|
|
166
178
|
self,
|
167
179
|
permissions: list[str],
|
168
180
|
) -> bool:
|
169
|
-
"""
|
170
|
-
Return True if no permissions are required or if at least one permission string is valid for the user.
|
171
|
-
|
172
|
-
If the permissions list is empty, access is granted. Otherwise, returns True if any permission string in the list is validated for the user; returns False if none are valid.
|
173
|
-
"""
|
181
|
+
"""Return True if any permission expression in the list evaluates to True."""
|
174
182
|
if not permissions:
|
175
183
|
return True
|
176
184
|
for permission in permissions:
|
@@ -181,17 +189,15 @@ class ManagerBasedPermission(BasePermission):
|
|
181
189
|
def getPermissionFilter(
|
182
190
|
self,
|
183
191
|
) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
|
184
|
-
"""
|
185
|
-
Returns the filter for the permission
|
186
|
-
"""
|
192
|
+
"""Return queryset filters inferred from class-level permission configuration."""
|
187
193
|
__based_on__ = getattr(self, "__based_on__")
|
188
194
|
filters: list[dict[Literal["filter", "exclude"], dict[str, str]]] = []
|
189
195
|
|
190
196
|
if self.__based_on_permission is not None:
|
191
197
|
base_permissions = self.__based_on_permission.getPermissionFilter()
|
192
|
-
for
|
193
|
-
filter =
|
194
|
-
exclude =
|
198
|
+
for base_permission in base_permissions:
|
199
|
+
filter = base_permission.get("filter", {})
|
200
|
+
exclude = base_permission.get("exclude", {})
|
195
201
|
filters.append(
|
196
202
|
{
|
197
203
|
"filter": {
|