django-ninja-aio-crud 1.0.1__tar.gz → 1.0.3__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.
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/PKG-INFO +3 -1
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/README.md +2 -1
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/exceptions.py +1 -1
- django_ninja_aio_crud-1.0.3/ninja_aio/helpers/__init__.py +3 -0
- django_ninja_aio_crud-1.0.3/ninja_aio/helpers/api.py +432 -0
- django_ninja_aio_crud-1.0.3/ninja_aio/models.py +1183 -0
- django_ninja_aio_crud-1.0.3/ninja_aio/renders.py +47 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/schemas.py +16 -1
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/views.py +14 -189
- django_ninja_aio_crud-1.0.1/ninja_aio/models.py +0 -887
- django_ninja_aio_crud-1.0.1/ninja_aio/renders.py +0 -54
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/LICENSE +0 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/decorators.py +0 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-1.0.1 → django_ninja_aio_crud-1.0.3}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
5
|
Author: Giuseppe Casillo
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -34,6 +34,7 @@ Provides-Extra: test
|
|
|
34
34
|
# 🥷 django-ninja-aio-crud
|
|
35
35
|
|
|
36
36
|

|
|
37
|
+
[](https://sonarcloud.io/summary/new_code?id=caspel26_django-ninja-aio-crud)
|
|
37
38
|
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
|
|
38
39
|
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
39
40
|
[](LICENSE)
|
|
@@ -334,3 +335,4 @@ MIT License. See [LICENSE](LICENSE).
|
|
|
334
335
|
| Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
|
|
335
336
|
|
|
336
337
|
---
|
|
338
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 🥷 django-ninja-aio-crud
|
|
2
2
|
|
|
3
3
|

|
|
4
|
+
[](https://sonarcloud.io/summary/new_code?id=caspel26_django-ninja-aio-crud)
|
|
4
5
|
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
|
|
5
6
|
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
6
7
|
[](LICENSE)
|
|
@@ -300,4 +301,4 @@ MIT License. See [LICENSE](LICENSE).
|
|
|
300
301
|
| Docs | https://django-ninja-aio.com
|
|
301
302
|
| Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
|
|
302
303
|
|
|
303
|
-
---
|
|
304
|
+
---
|
|
@@ -42,7 +42,7 @@ class NotFoundError(BaseException):
|
|
|
42
42
|
|
|
43
43
|
def __init__(self, model: Model, details=None):
|
|
44
44
|
super().__init__(
|
|
45
|
-
error={model._meta.verbose_name: self.error},
|
|
45
|
+
error={model._meta.verbose_name.replace(" ", "_"): self.error},
|
|
46
46
|
status_code=self.status_code,
|
|
47
47
|
details=details,
|
|
48
48
|
)
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Coroutine
|
|
3
|
+
|
|
4
|
+
from django.http import HttpRequest
|
|
5
|
+
from ninja import Path, Query
|
|
6
|
+
from ninja.pagination import paginate
|
|
7
|
+
from ninja_aio.decorators import unique_view
|
|
8
|
+
from ninja_aio.models import ModelSerializer, ModelUtil
|
|
9
|
+
from ninja_aio.schemas import (
|
|
10
|
+
GenericMessageSchema,
|
|
11
|
+
M2MRelationSchema,
|
|
12
|
+
M2MSchemaIn,
|
|
13
|
+
M2MSchemaOut,
|
|
14
|
+
M2MAddSchemaIn,
|
|
15
|
+
M2MRemoveSchemaIn,
|
|
16
|
+
)
|
|
17
|
+
from django.db.models import QuerySet, Model
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ManyToManyAPI:
|
|
21
|
+
"""
|
|
22
|
+
ManyToManyAPI
|
|
23
|
+
-------------
|
|
24
|
+
WARNING (Internal Use Only):
|
|
25
|
+
This helper is currently intended solely for internal purposes. Its API,
|
|
26
|
+
behaviors, and response formats may change without notice. Do not rely on
|
|
27
|
+
it as a stable public interface.
|
|
28
|
+
|
|
29
|
+
Utility class that dynamically attaches asynchronous Many-To-Many (M2M) management
|
|
30
|
+
endpoints (GET / ADD / REMOVE) to a provided APIViewSet router in a Django Ninja
|
|
31
|
+
async CRUD context.
|
|
32
|
+
|
|
33
|
+
It inspects a list of M2MRelationSchema definitions and, for each relation, builds:
|
|
34
|
+
- An optional paginated GET endpoint to list related objects.
|
|
35
|
+
- An optional POST endpoint to add and/or remove related object primary keys.
|
|
36
|
+
|
|
37
|
+
Core behaviors:
|
|
38
|
+
- Dynamically generates per-relation filter schemas for query parameters.
|
|
39
|
+
- Supports custom per-relation query filtering handlers on the parent view set
|
|
40
|
+
via a `{related_name}_query_params_handler` coroutine.
|
|
41
|
+
- Validates requested add/remove primary keys, producing granular success and
|
|
42
|
+
error feedback.
|
|
43
|
+
- Performs add/remove operations concurrently using asyncio.gather when both
|
|
44
|
+
types of operations are requested in the same call.
|
|
45
|
+
|
|
46
|
+
Attributes established at initialization:
|
|
47
|
+
relations: list of M2MRelationSchema defining each relation.
|
|
48
|
+
view_set: The parent APIViewSet instance from which router, pagination, model util,
|
|
49
|
+
and path schema are derived.
|
|
50
|
+
router: Ninja router used to register generated endpoints.
|
|
51
|
+
pagination_class: Pagination class used for GET related endpoints.
|
|
52
|
+
path_schema: Pydantic schema used to validate path parameters (e.g., primary key).
|
|
53
|
+
related_model_util: A ModelUtil instance cloned from the parent view set to access
|
|
54
|
+
base object retrieval helpers.
|
|
55
|
+
relations_filters_schemas: Mapping of related_name -> generated Pydantic filter schema.
|
|
56
|
+
|
|
57
|
+
Generated endpoint naming conventions:
|
|
58
|
+
GET -> get_{base_model_name}_{relation_path}
|
|
59
|
+
POST -> manage_{base_model_name}_{relation_path}
|
|
60
|
+
|
|
61
|
+
All responses standardize success and error reporting for POST as:
|
|
62
|
+
{
|
|
63
|
+
"results": {"count": int, "details": [str, ...]},
|
|
64
|
+
"errors": {"count": int, "details": [str, ...]}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Concurrency note:
|
|
68
|
+
Add and remove operations are executed concurrently when both lists are non-empty,
|
|
69
|
+
minimizing round-trip latency for bulk mutations.
|
|
70
|
+
|
|
71
|
+
Error semantics:
|
|
72
|
+
- Missing related objects: reported individually.
|
|
73
|
+
- Invalid operation context (e.g., removing objects not currently related or adding
|
|
74
|
+
objects already related) reported per primary key.
|
|
75
|
+
- Successful operations yield a corresponding success detail string per PK.
|
|
76
|
+
|
|
77
|
+
Security / auth:
|
|
78
|
+
- Each relation may optionally override auth via its schema; otherwise falls back
|
|
79
|
+
to a default configured on the instance (self.default_auth).
|
|
80
|
+
|
|
81
|
+
Pagination:
|
|
82
|
+
- Applied only to GET related endpoints via @paginate(self.pagination_class).
|
|
83
|
+
|
|
84
|
+
Extensibility:
|
|
85
|
+
- Provide custom query param handling by defining an async method on the parent
|
|
86
|
+
view set: `<related_name>_query_params_handler(self, queryset, filters_dict)`.
|
|
87
|
+
- Customize relation filtering schema via each relation's `filters` definition.
|
|
88
|
+
|
|
89
|
+
-----------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
__init__(relations, view_set)
|
|
92
|
+
Initialize the M2M API helper by binding core utilities from the provided view set
|
|
93
|
+
and precomputing filter schemas per relation.
|
|
94
|
+
|
|
95
|
+
Parameters:
|
|
96
|
+
relations (list[M2MRelationSchema]): Definitions for each M2M relation to expose.
|
|
97
|
+
view_set (APIViewSet): Parent view set containing model utilities and router.
|
|
98
|
+
|
|
99
|
+
Side effects:
|
|
100
|
+
- Captures router, pagination class, path schema from view_set.
|
|
101
|
+
- Clones ModelUtil for related model operations.
|
|
102
|
+
- Pre-generates filter schemas for each relation (if filters declared).
|
|
103
|
+
|
|
104
|
+
-----------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
_generate_m2m_filters_schemas()
|
|
107
|
+
Create a mapping of related_name -> Pydantic schema used for query filtering in
|
|
108
|
+
GET related endpoints. If a relation has no filters specified, an empty schema
|
|
109
|
+
(dict) is used.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
dict[str, BaseModel]: Generated schemas keyed by related_name.
|
|
113
|
+
|
|
114
|
+
-----------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
_get_query_handler(related_name)
|
|
117
|
+
Retrieve an optional per-relation query handler coroutine from the parent view set.
|
|
118
|
+
Naming convention: `<related_name>_query_params_handler`.
|
|
119
|
+
|
|
120
|
+
Parameters:
|
|
121
|
+
related_name (str): The relation's attribute name on the base model.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Coroutine | None: Handler to transform or filter the queryset based on query params.
|
|
125
|
+
|
|
126
|
+
-----------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
_check_m2m_objs(request, objs_pks, model, related_manager, remove=False)
|
|
129
|
+
Validate requested primary keys for add/remove operations against the current
|
|
130
|
+
relation state. Performs existence checks and logical consistency (e.g., prevents
|
|
131
|
+
adding already-related objects or removing non-related objects).
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
request (HttpRequest): Incoming request context (passed to ModelUtil for access control).
|
|
135
|
+
objs_pks (list): List of primary keys to add or remove.
|
|
136
|
+
model (ModelSerializer | Model): Model class or serializer used to resolve objects.
|
|
137
|
+
related_manager (QuerySet): Related manager for the base object's M2M field.
|
|
138
|
+
remove (bool): If True, treat operation as removal validation.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
tuple[list[str], list[str], list[Model]]:
|
|
142
|
+
errors -> List of error messages per invalid PK.
|
|
143
|
+
objs_detail -> List of success detail messages per valid PK.
|
|
144
|
+
objs -> List of resolved model instances to process.
|
|
145
|
+
|
|
146
|
+
Error cases:
|
|
147
|
+
- Object not found.
|
|
148
|
+
- Object presence mismatch (attempting wrong operation given relation membership).
|
|
149
|
+
|
|
150
|
+
-----------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
_collect_m2m(request, pks, model, related_manager, remove=False)
|
|
153
|
+
Wrapper around _check_m2m_objs that short-circuits on empty PK lists.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
request (HttpRequest)
|
|
157
|
+
pks (list): Primary keys proposed for mutation.
|
|
158
|
+
model (ModelSerializer | Model)
|
|
159
|
+
related_manager (QuerySet)
|
|
160
|
+
remove (bool): Operation type flag.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
tuple[list[str], list[str], list[Model]]: See _check_m2m_objs.
|
|
164
|
+
|
|
165
|
+
-----------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
_build_views(relation)
|
|
168
|
+
Dynamically define and register the GET and/or POST endpoints for a single M2M
|
|
169
|
+
relation based on the relation's schema flags (get/add/remove). Builds filter
|
|
170
|
+
schemas, resolves path fragments, and binds handlers to the router with unique
|
|
171
|
+
operation IDs.
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
relation (M2MRelationSchema): Declarative specification for one M2M relation.
|
|
175
|
+
|
|
176
|
+
Side effects:
|
|
177
|
+
- Registers endpoints on self.router.
|
|
178
|
+
- Creates closures (get_related / manage_related) capturing relation context.
|
|
179
|
+
|
|
180
|
+
GET endpoint behavior:
|
|
181
|
+
- Retrieves base object via related_model_util.
|
|
182
|
+
- Fetches all related objects; applies optional query handler and filters.
|
|
183
|
+
- Serializes each related object with rel_util.read_s.
|
|
184
|
+
|
|
185
|
+
POST endpoint behavior:
|
|
186
|
+
- Parses add/remove PK lists.
|
|
187
|
+
- Validates objects via _collect_m2m.
|
|
188
|
+
- Performs asynchronous add/remove operations using aadd / aremove.
|
|
189
|
+
- Aggregates results and errors into standardized response payload.
|
|
190
|
+
|
|
191
|
+
-----------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
_add_views()
|
|
194
|
+
Iterates over all declared relations and invokes _build_views to attach endpoints.
|
|
195
|
+
|
|
196
|
+
Side effects:
|
|
197
|
+
- Populates router with all required M2M endpoints.
|
|
198
|
+
|
|
199
|
+
-----------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
Usage Example (conceptual):
|
|
202
|
+
api = ManyToManyAPI(relations=[...], view_set=my_view_set)
|
|
203
|
+
api._add_views() # Registers all endpoints automatically during initialization flow.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
relations: list[M2MRelationSchema],
|
|
209
|
+
view_set,
|
|
210
|
+
):
|
|
211
|
+
# Import here to avoid circular imports
|
|
212
|
+
from ninja_aio.views import APIViewSet
|
|
213
|
+
|
|
214
|
+
self.relations = relations
|
|
215
|
+
self.view_set: APIViewSet = view_set
|
|
216
|
+
self.router = self.view_set.router
|
|
217
|
+
self.pagination_class = self.view_set.pagination_class
|
|
218
|
+
self.path_schema = self.view_set.path_schema
|
|
219
|
+
self.default_auth = self.view_set.m2m_auth
|
|
220
|
+
self.related_model_util = self.view_set.model_util
|
|
221
|
+
self.relations_filters_schemas = self._generate_m2m_filters_schemas()
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def views_action_map(self):
|
|
225
|
+
return {
|
|
226
|
+
(True, True): ("Add or Remove", M2MSchemaIn),
|
|
227
|
+
(True, False): ("Add", M2MAddSchemaIn),
|
|
228
|
+
(False, True): ("Remove", M2MRemoveSchemaIn),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def _generate_m2m_filters_schemas(self):
|
|
232
|
+
"""
|
|
233
|
+
Build per-relation filters schemas for M2M endpoints.
|
|
234
|
+
"""
|
|
235
|
+
return {
|
|
236
|
+
data.related_name: self.view_set._generate_schema(
|
|
237
|
+
{} if not data.filters else data.filters,
|
|
238
|
+
f"{self.related_model_util.model_name}{data.related_name.capitalize()}FiltersSchema",
|
|
239
|
+
)
|
|
240
|
+
for data in self.relations
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def _get_query_handler(self, related_name: str) -> Coroutine | None:
|
|
244
|
+
return getattr(self.view_set, f"{related_name}_query_params_handler", None)
|
|
245
|
+
|
|
246
|
+
async def _check_m2m_objs(
|
|
247
|
+
self,
|
|
248
|
+
request: HttpRequest,
|
|
249
|
+
objs_pks: list,
|
|
250
|
+
model: ModelSerializer | Model,
|
|
251
|
+
related_manager: QuerySet,
|
|
252
|
+
remove: bool = False,
|
|
253
|
+
):
|
|
254
|
+
"""
|
|
255
|
+
Validate requested add/remove pk list for M2M operations.
|
|
256
|
+
Returns (errors, details, objects_to_process).
|
|
257
|
+
"""
|
|
258
|
+
errors, objs_detail, objs = [], [], []
|
|
259
|
+
rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
|
|
260
|
+
rel_model_name = model._meta.verbose_name.capitalize()
|
|
261
|
+
for obj_pk in objs_pks:
|
|
262
|
+
rel_obj = await (
|
|
263
|
+
await ModelUtil(model).get_object(request, filters={"pk": obj_pk})
|
|
264
|
+
).afirst()
|
|
265
|
+
if rel_obj is None:
|
|
266
|
+
errors.append(f"{rel_model_name} with pk {obj_pk} not found.")
|
|
267
|
+
continue
|
|
268
|
+
if remove ^ (rel_obj in rel_objs):
|
|
269
|
+
errors.append(
|
|
270
|
+
f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''}in {self.related_model_util.model_name}"
|
|
271
|
+
)
|
|
272
|
+
continue
|
|
273
|
+
objs.append(rel_obj)
|
|
274
|
+
objs_detail.append(
|
|
275
|
+
f"{rel_model_name} with id {obj_pk} successfully {'removed' if remove else 'added'}"
|
|
276
|
+
)
|
|
277
|
+
return errors, objs_detail, objs
|
|
278
|
+
|
|
279
|
+
async def _collect_m2m(
|
|
280
|
+
self,
|
|
281
|
+
request: HttpRequest,
|
|
282
|
+
pks: list,
|
|
283
|
+
model: ModelSerializer | Model,
|
|
284
|
+
related_manager: QuerySet,
|
|
285
|
+
remove=False,
|
|
286
|
+
):
|
|
287
|
+
if not pks:
|
|
288
|
+
return ([], [], [])
|
|
289
|
+
return await self._check_m2m_objs(
|
|
290
|
+
request, pks, model, related_manager, remove=remove
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _register_get_relation_view(
|
|
294
|
+
self,
|
|
295
|
+
*,
|
|
296
|
+
related_name: str,
|
|
297
|
+
m2m_auth,
|
|
298
|
+
rel_util: ModelUtil,
|
|
299
|
+
rel_path: str,
|
|
300
|
+
related_schema,
|
|
301
|
+
filters_schema,
|
|
302
|
+
):
|
|
303
|
+
@self.router.get(
|
|
304
|
+
f"{self.view_set.path_retrieve}{rel_path}",
|
|
305
|
+
response={
|
|
306
|
+
200: list[related_schema],
|
|
307
|
+
self.view_set.error_codes: GenericMessageSchema,
|
|
308
|
+
},
|
|
309
|
+
auth=m2m_auth,
|
|
310
|
+
summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
311
|
+
description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
312
|
+
)
|
|
313
|
+
@unique_view(f"get_{self.related_model_util.model_name}_{rel_path}")
|
|
314
|
+
@paginate(self.pagination_class)
|
|
315
|
+
async def get_related(
|
|
316
|
+
request: HttpRequest,
|
|
317
|
+
pk: Path[self.path_schema], # type: ignore
|
|
318
|
+
filters: Query[filters_schema] = None, # type: ignore
|
|
319
|
+
):
|
|
320
|
+
obj = await self.related_model_util.get_object(
|
|
321
|
+
request, self.view_set._get_pk(pk)
|
|
322
|
+
)
|
|
323
|
+
related_manager = getattr(obj, related_name)
|
|
324
|
+
related_qs = related_manager.all()
|
|
325
|
+
|
|
326
|
+
query_handler = self._get_query_handler(related_name)
|
|
327
|
+
if filters is not None and query_handler:
|
|
328
|
+
related_qs = await query_handler(related_qs, filters.model_dump())
|
|
329
|
+
|
|
330
|
+
return [
|
|
331
|
+
await rel_util.read_s(request, rel_obj, related_schema)
|
|
332
|
+
async for rel_obj in related_qs
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
def _resolve_action_schema(self, add: bool, remove: bool):
|
|
336
|
+
return self.views_action_map[(add, remove)]
|
|
337
|
+
|
|
338
|
+
def _register_manage_relation_view(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
model,
|
|
342
|
+
related_name: str,
|
|
343
|
+
m2m_auth,
|
|
344
|
+
rel_util: ModelUtil,
|
|
345
|
+
rel_path: str,
|
|
346
|
+
m2m_add: bool,
|
|
347
|
+
m2m_remove: bool,
|
|
348
|
+
):
|
|
349
|
+
action, schema_in = self._resolve_action_schema(m2m_add, m2m_remove)
|
|
350
|
+
plural = rel_util.model._meta.verbose_name_plural.capitalize()
|
|
351
|
+
summary = f"{action} {plural}"
|
|
352
|
+
|
|
353
|
+
@self.router.post(
|
|
354
|
+
f"{self.view_set.path_retrieve}{rel_path}/",
|
|
355
|
+
response={
|
|
356
|
+
200: M2MSchemaOut,
|
|
357
|
+
self.view_set.error_codes: GenericMessageSchema,
|
|
358
|
+
},
|
|
359
|
+
auth=m2m_auth,
|
|
360
|
+
summary=summary,
|
|
361
|
+
description=summary,
|
|
362
|
+
)
|
|
363
|
+
@unique_view(f"manage_{self.related_model_util.model_name}_{rel_path}")
|
|
364
|
+
async def manage_related(
|
|
365
|
+
request: HttpRequest,
|
|
366
|
+
pk: Path[self.path_schema], # type: ignore
|
|
367
|
+
data: schema_in, # type: ignore
|
|
368
|
+
):
|
|
369
|
+
obj = await self.related_model_util.get_object(
|
|
370
|
+
request, self.view_set._get_pk(pk)
|
|
371
|
+
)
|
|
372
|
+
related_manager: QuerySet = getattr(obj, related_name)
|
|
373
|
+
|
|
374
|
+
add_pks = getattr(data, "add", []) if m2m_add else []
|
|
375
|
+
remove_pks = getattr(data, "remove", []) if m2m_remove else []
|
|
376
|
+
|
|
377
|
+
add_errors, add_details, add_objs = await self._collect_m2m(
|
|
378
|
+
request, add_pks, model, related_manager
|
|
379
|
+
)
|
|
380
|
+
remove_errors, remove_details, remove_objs = await self._collect_m2m(
|
|
381
|
+
request, remove_pks, model, related_manager, remove=True
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
tasks = []
|
|
385
|
+
if add_objs:
|
|
386
|
+
tasks.append(related_manager.aadd(*add_objs))
|
|
387
|
+
if remove_objs:
|
|
388
|
+
tasks.append(related_manager.aremove(*remove_objs))
|
|
389
|
+
if tasks:
|
|
390
|
+
await asyncio.gather(*tasks)
|
|
391
|
+
|
|
392
|
+
results = add_details + remove_details
|
|
393
|
+
errors = add_errors + remove_errors
|
|
394
|
+
return {
|
|
395
|
+
"results": {"count": len(results), "details": results},
|
|
396
|
+
"errors": {"count": len(errors), "details": errors},
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
def _build_views(self, relation: M2MRelationSchema):
|
|
400
|
+
model = relation.model
|
|
401
|
+
related_name = relation.related_name
|
|
402
|
+
m2m_auth = relation.auth or self.default_auth
|
|
403
|
+
rel_util = ModelUtil(model)
|
|
404
|
+
rel_path = relation.path or rel_util.verbose_name_path_resolver()
|
|
405
|
+
related_schema = relation.related_schema
|
|
406
|
+
m2m_add, m2m_remove, m2m_get = relation.add, relation.remove, relation.get
|
|
407
|
+
filters_schema = self.relations_filters_schemas.get(related_name)
|
|
408
|
+
|
|
409
|
+
if m2m_get:
|
|
410
|
+
self._register_get_relation_view(
|
|
411
|
+
related_name=related_name,
|
|
412
|
+
m2m_auth=m2m_auth,
|
|
413
|
+
rel_util=rel_util,
|
|
414
|
+
rel_path=rel_path,
|
|
415
|
+
related_schema=related_schema,
|
|
416
|
+
filters_schema=filters_schema,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if m2m_add or m2m_remove:
|
|
420
|
+
self._register_manage_relation_view(
|
|
421
|
+
model=model,
|
|
422
|
+
related_name=related_name,
|
|
423
|
+
m2m_auth=m2m_auth,
|
|
424
|
+
rel_util=rel_util,
|
|
425
|
+
rel_path=rel_path,
|
|
426
|
+
m2m_add=m2m_add,
|
|
427
|
+
m2m_remove=m2m_remove,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def _add_views(self):
|
|
431
|
+
for relation in self.relations:
|
|
432
|
+
self._build_views(relation)
|