django-ninja-aio-crud 0.11.4__py3-none-any.whl → 1.0.1__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.
- django_ninja_aio_crud-1.0.1.dist-info/METADATA +336 -0
- {django_ninja_aio_crud-0.11.4.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/RECORD +7 -7
- ninja_aio/__init__.py +1 -1
- ninja_aio/schemas.py +40 -2
- ninja_aio/views.py +276 -204
- django_ninja_aio_crud-0.11.4.dist-info/METADATA +0 -527
- {django_ninja_aio_crud-0.11.4.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-0.11.4.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/licenses/LICENSE +0 -0
ninja_aio/views.py
CHANGED
|
@@ -15,6 +15,7 @@ from .schemas import (
|
|
|
15
15
|
M2MSchemaIn,
|
|
16
16
|
M2MAddSchemaIn,
|
|
17
17
|
M2MRemoveSchemaIn,
|
|
18
|
+
M2MRelationSchema,
|
|
18
19
|
)
|
|
19
20
|
from .types import ModelSerializerMeta, VIEW_TYPES
|
|
20
21
|
from .decorators import unique_view
|
|
@@ -75,64 +76,86 @@ class APIView:
|
|
|
75
76
|
|
|
76
77
|
class APIViewSet:
|
|
77
78
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
This class provides methods for creating, listing, retrieving, updating,
|
|
81
|
-
and deleting objects of a specified model. It supports pagination,
|
|
82
|
-
authentication, and custom query parameters.
|
|
83
|
-
|
|
84
|
-
## Attributes:
|
|
85
|
-
- **model** (`ModelSerializer | Model`): The model for CRUD operations.
|
|
86
|
-
- **api** (`NinjaAPI`): The API instance to which the views are added.
|
|
87
|
-
- **schema_in** (`Schema | None`): Schema for input data in create/update operations.
|
|
88
|
-
- **schema_out** (`Schema | None`): Schema for output data in list/retrieve operations.
|
|
89
|
-
- **schema_update** (`Schema | None`): Schema for update operations.
|
|
90
|
-
- **auth** (`list | None`): Authentication classes for the views.
|
|
91
|
-
- **get_auth** (`list | None`): Authentication for GET requests.
|
|
92
|
-
- **post_auth** (`list | None`): Authentication for POST requests.
|
|
93
|
-
- **patch_auth** (`list | None`): Authentication for PATCH requests.
|
|
94
|
-
- **delete_auth** (`list | None`): Authentication for DELETE requests.
|
|
95
|
-
- **pagination_class** (`type[AsyncPaginationBase]`): Pagination class to use.
|
|
96
|
-
- **query_params** (`dict[str, tuple[type, ...]]`): Query parameters for filtering.
|
|
97
|
-
- **disable** (`list[type[VIEW_TYPES]]`): List of view types to disable.
|
|
98
|
-
- **api_route_path** (`str`): Base path for the API route.
|
|
99
|
-
- **list_docs** (`str`): Documentation for the list view.
|
|
100
|
-
- **create_docs** (`str`): Documentation for the create view.
|
|
101
|
-
- **retrieve_docs** (`str`): Documentation for the retrieve view.
|
|
102
|
-
- **update_docs** (`str`): Documentation for the update view.
|
|
103
|
-
- **delete_docs** (`str`): Documentation for the delete view.
|
|
104
|
-
- **m2m_relations** (`list[tuple[ModelSerializer | Model, str, str, list]]`): Many-to-many relations to manage. Each tuple contains:
|
|
105
|
-
- The related model.
|
|
106
|
-
- The related name on the main model.
|
|
107
|
-
- The name of the path, if not provided a default will be used.
|
|
108
|
-
- The authentication for the m2m views.
|
|
109
|
-
- **m2m_add** (`bool`): Enable add operation for M2M relations.
|
|
110
|
-
- **m2m_remove** (`bool`): Enable remove operation for M2M relations.
|
|
111
|
-
- **m2m_get** (`bool`): Enable get operation for M2M relations.
|
|
112
|
-
- **m2m_auth** (`list | None`): Authentication for M2M views.
|
|
113
|
-
|
|
114
|
-
## Notes:
|
|
115
|
-
If the model is a ModelSerializer instance, schemas are generated
|
|
116
|
-
automatically based on Create, Read, and Update serializers.
|
|
117
|
-
Override the `views` method to add custom views.
|
|
118
|
-
Override the `query_params_handler` method to handle query params
|
|
119
|
-
and return a filtered queryset.
|
|
120
|
-
|
|
121
|
-
## Methods:
|
|
122
|
-
- **create_view**: Creates a new object.
|
|
123
|
-
- **list_view**: Lists all objects.
|
|
124
|
-
- **retrieve_view**: Retrieves an object by its primary key.
|
|
125
|
-
- **update_view**: Updates an object by its primary key.
|
|
126
|
-
- **delete_view**: Deletes an object by its primary key.
|
|
127
|
-
- **views**: Override to add custom views.
|
|
128
|
-
- **add_views_to_route**: Adds the views to the API route.
|
|
129
|
-
|
|
130
|
-
## Example:
|
|
131
|
-
class MyModelViewSet(APIViewSet):
|
|
132
|
-
model = MyModel # Your Django model
|
|
133
|
-
api = my_api_instance # Your NinjaAPI instance
|
|
79
|
+
Base viewset generating async CRUD + optional M2M endpoints for a Django model.
|
|
134
80
|
|
|
81
|
+
Usage:
|
|
82
|
+
class MyModelViewSet(APIViewSet):
|
|
83
|
+
model = MyModel
|
|
84
|
+
api = api
|
|
135
85
|
MyModelViewSet().add_views_to_route()
|
|
86
|
+
|
|
87
|
+
Automatic schema generation:
|
|
88
|
+
If model is a ModelSerializer (subclass of ModelSerializerMeta),
|
|
89
|
+
read/create/update schemas are auto-generated from its serializers.
|
|
90
|
+
Otherwise provide schema_in / schema_out / schema_update manually.
|
|
91
|
+
|
|
92
|
+
Generated endpoints (unless disabled via `disable`):
|
|
93
|
+
POST / -> create_view (201, schema_out)
|
|
94
|
+
GET / -> list_view (200, List[schema_out] paginated)
|
|
95
|
+
GET /{pk} -> retrieve_view (200, schema_out)
|
|
96
|
+
PATCH /{pk}/ -> update_view (200, schema_out)
|
|
97
|
+
DELETE /{pk}/ -> delete_view (204)
|
|
98
|
+
|
|
99
|
+
M2M endpoints (per entry in m2m_relations) if enabled:
|
|
100
|
+
GET /{pk}/{related_path} -> list related objects (paginated)
|
|
101
|
+
POST /{pk}/{related_path}/ -> add/remove related objects (depending on m2m_add / m2m_remove)
|
|
102
|
+
|
|
103
|
+
M2M filters:
|
|
104
|
+
Each M2MRelationSchema may define a filters dict:
|
|
105
|
+
filters = { "field_name": (type, default) }
|
|
106
|
+
A dynamic Pydantic Filters schema is generated and exposed as query params
|
|
107
|
+
on the related GET endpoint: /{pk}/{related_path}?field_name=value.
|
|
108
|
+
To apply custom filter logic implement an async hook named:
|
|
109
|
+
<related_name>_query_params_handler(self, queryset, filters_dict)
|
|
110
|
+
It receives the initial related queryset and the validated/dumped filters
|
|
111
|
+
dict, and must return the (optionally) filtered queryset.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
class UserViewSet(APIViewSet):
|
|
115
|
+
model = models.User
|
|
116
|
+
m2m_relations = [
|
|
117
|
+
M2MRelationSchema(
|
|
118
|
+
model=models.Tag,
|
|
119
|
+
related_name="tags",
|
|
120
|
+
filters={
|
|
121
|
+
"name": (str, "")
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
async def tags_query_params_handler(self, queryset, filters):
|
|
127
|
+
name_filter = filters.get("name")
|
|
128
|
+
if name_filter:
|
|
129
|
+
queryset = queryset.filter(name__icontains=name_filter)
|
|
130
|
+
return queryset
|
|
131
|
+
|
|
132
|
+
If filters is empty or omitted no query params are added for that relation.
|
|
133
|
+
|
|
134
|
+
Attribute summary:
|
|
135
|
+
model: Django model or ModelSerializer.
|
|
136
|
+
api: NinjaAPI instance.
|
|
137
|
+
schema_in / schema_out / schema_update: Pydantic schemas (auto when ModelSerializer).
|
|
138
|
+
auth: Default auth list or NOT_SET (no auth). Verb specific auth: get_auth, post_auth, patch_auth, delete_auth.
|
|
139
|
+
pagination_class: AsyncPaginationBase subclass (default PageNumberPagination).
|
|
140
|
+
query_params: Dict[str, (type, default)] to build a dynamic filters schema for list_view.
|
|
141
|
+
disable: List of view type strings: 'create','list','retrieve','update','delete','all'.
|
|
142
|
+
api_route_path: Base path; auto-resolved from verbose name if empty.
|
|
143
|
+
list_docs / create_docs / retrieve_docs / update_docs / delete_docs: Endpoint descriptions.
|
|
144
|
+
m2m_relations: List of M2MRelationSchema describing related model, related_name, custom path, auth, filters.
|
|
145
|
+
m2m_add / m2m_remove / m2m_get: Enable add/remove/get M2M operations.
|
|
146
|
+
m2m_auth: Auth list for all M2M endpoints unless overridden per relation.
|
|
147
|
+
|
|
148
|
+
Overridable hooks:
|
|
149
|
+
views(): Register extra custom endpoints on self.router.
|
|
150
|
+
query_params_handler(queryset, filters): Async hook to apply list filters.
|
|
151
|
+
<related_name>_query_params_handler(queryset, filters): Async hook for per-M2M filtering.
|
|
152
|
+
|
|
153
|
+
Error responses:
|
|
154
|
+
All endpoints may return GenericMessageSchema for codes in ERROR_CODES (400,401,404,428).
|
|
155
|
+
|
|
156
|
+
Internal:
|
|
157
|
+
Dynamic path/filter schemas built with pydantic.create_model.
|
|
158
|
+
unique_view decorator prevents duplicate registration.
|
|
136
159
|
"""
|
|
137
160
|
|
|
138
161
|
model: ModelSerializer | Model
|
|
@@ -154,10 +177,7 @@ class APIViewSet:
|
|
|
154
177
|
retrieve_docs = "Retrieve a specific object by its primary key."
|
|
155
178
|
update_docs = "Update an object by its primary key."
|
|
156
179
|
delete_docs = "Delete an object by its primary key."
|
|
157
|
-
m2m_relations: list[
|
|
158
|
-
m2m_add = True
|
|
159
|
-
m2m_remove = True
|
|
160
|
-
m2m_get = True
|
|
180
|
+
m2m_relations: list[M2MRelationSchema] = []
|
|
161
181
|
m2m_auth: list | None = NOT_SET
|
|
162
182
|
|
|
163
183
|
def __init__(self) -> None:
|
|
@@ -166,6 +186,7 @@ class APIViewSet:
|
|
|
166
186
|
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
167
187
|
self.path_schema = self._generate_path_schema()
|
|
168
188
|
self.filters_schema = self._generate_filters_schema()
|
|
189
|
+
self.m2m_filters_schemas = self._generate_m2m_filters_schemas()
|
|
169
190
|
self.model_verbose_name = self.model._meta.verbose_name.capitalize()
|
|
170
191
|
self.router_tag = self.model_verbose_name
|
|
171
192
|
self.router = Router(tags=[self.router_tag])
|
|
@@ -180,8 +201,7 @@ class APIViewSet:
|
|
|
180
201
|
@property
|
|
181
202
|
def _crud_views(self):
|
|
182
203
|
"""
|
|
183
|
-
|
|
184
|
-
value: tuple with schema and view method
|
|
204
|
+
Mapping of CRUD operation name to (response schema, view factory).
|
|
185
205
|
"""
|
|
186
206
|
return {
|
|
187
207
|
"create": (self.schema_in, self.create_view),
|
|
@@ -192,6 +212,9 @@ class APIViewSet:
|
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
def _auth_view(self, view_type: str):
|
|
215
|
+
"""
|
|
216
|
+
Resolve auth for a specific HTTP verb; falls back to self.auth if NOT_SET.
|
|
217
|
+
"""
|
|
195
218
|
auth = getattr(self, f"{view_type}_auth", None)
|
|
196
219
|
return auth if auth is not NOT_SET else self.auth
|
|
197
220
|
|
|
@@ -208,20 +231,47 @@ class APIViewSet:
|
|
|
208
231
|
return self._auth_view("delete")
|
|
209
232
|
|
|
210
233
|
def _generate_schema(self, fields: dict, name: str) -> Schema:
|
|
234
|
+
"""
|
|
235
|
+
Dynamically build a Pydantic model for path / filter schemas.
|
|
236
|
+
"""
|
|
211
237
|
return create_model(f"{self.model_util.model_name}{name}", **fields)
|
|
212
238
|
|
|
213
239
|
def _generate_path_schema(self):
|
|
240
|
+
"""
|
|
241
|
+
Schema containing only the primary key field for path resolution.
|
|
242
|
+
"""
|
|
214
243
|
return self._generate_schema(
|
|
215
244
|
{self.model_util.model_pk_name: (int | str, ...)}, "PathSchema"
|
|
216
245
|
)
|
|
217
246
|
|
|
218
247
|
def _generate_filters_schema(self):
|
|
248
|
+
"""
|
|
249
|
+
Build filters schema from query_params definition.
|
|
250
|
+
"""
|
|
219
251
|
return self._generate_schema(self.query_params, "FiltersSchema")
|
|
220
252
|
|
|
253
|
+
def _generate_m2m_filters_schemas(self):
|
|
254
|
+
"""
|
|
255
|
+
Build per-relation filters schemas for M2M endpoints.
|
|
256
|
+
"""
|
|
257
|
+
return {
|
|
258
|
+
m2m_data.related_name: self._generate_schema(
|
|
259
|
+
{} if not m2m_data.filters else m2m_data.filters,
|
|
260
|
+
f"{self.model_util.model_name}{m2m_data.related_name.capitalize()}FiltersSchema",
|
|
261
|
+
)
|
|
262
|
+
for m2m_data in self.m2m_relations
|
|
263
|
+
}
|
|
264
|
+
|
|
221
265
|
def _get_pk(self, data: Schema):
|
|
266
|
+
"""
|
|
267
|
+
Extract pk from a path schema instance.
|
|
268
|
+
"""
|
|
222
269
|
return data.model_dump()[self.model_util.model_pk_name]
|
|
223
270
|
|
|
224
271
|
def get_schemas(self):
|
|
272
|
+
"""
|
|
273
|
+
Return (schema_out, schema_in, schema_update), generating them if model is a ModelSerializer.
|
|
274
|
+
"""
|
|
225
275
|
if isinstance(self.model, ModelSerializerMeta):
|
|
226
276
|
return (
|
|
227
277
|
self.model.generate_read_s(),
|
|
@@ -234,13 +284,17 @@ class APIViewSet:
|
|
|
234
284
|
self, queryset: QuerySet[ModelSerializer], filters: dict
|
|
235
285
|
):
|
|
236
286
|
"""
|
|
237
|
-
Override
|
|
238
|
-
|
|
239
|
-
|
|
287
|
+
Override to apply custom filtering logic for list_view.
|
|
288
|
+
filters is already validated and dumped.
|
|
289
|
+
Return the (possibly modified) queryset.
|
|
240
290
|
"""
|
|
241
291
|
return queryset
|
|
242
292
|
|
|
243
293
|
def create_view(self):
|
|
294
|
+
"""
|
|
295
|
+
Register create endpoint.
|
|
296
|
+
"""
|
|
297
|
+
|
|
244
298
|
@self.router.post(
|
|
245
299
|
self.path,
|
|
246
300
|
auth=self.post_view_auth(),
|
|
@@ -255,6 +309,10 @@ class APIViewSet:
|
|
|
255
309
|
return create
|
|
256
310
|
|
|
257
311
|
def list_view(self):
|
|
312
|
+
"""
|
|
313
|
+
Register list endpoint with pagination and optional filters.
|
|
314
|
+
"""
|
|
315
|
+
|
|
258
316
|
@self.router.get(
|
|
259
317
|
self.get_path,
|
|
260
318
|
auth=self.get_view_auth(),
|
|
@@ -288,6 +346,10 @@ class APIViewSet:
|
|
|
288
346
|
return list
|
|
289
347
|
|
|
290
348
|
def retrieve_view(self):
|
|
349
|
+
"""
|
|
350
|
+
Register retrieve endpoint.
|
|
351
|
+
"""
|
|
352
|
+
|
|
291
353
|
@self.router.get(
|
|
292
354
|
self.get_path_retrieve,
|
|
293
355
|
auth=self.get_view_auth(),
|
|
@@ -303,6 +365,10 @@ class APIViewSet:
|
|
|
303
365
|
return retrieve
|
|
304
366
|
|
|
305
367
|
def update_view(self):
|
|
368
|
+
"""
|
|
369
|
+
Register update endpoint.
|
|
370
|
+
"""
|
|
371
|
+
|
|
306
372
|
@self.router.patch(
|
|
307
373
|
self.path_retrieve,
|
|
308
374
|
auth=self.patch_view_auth(),
|
|
@@ -323,6 +389,10 @@ class APIViewSet:
|
|
|
323
389
|
return update
|
|
324
390
|
|
|
325
391
|
def delete_view(self):
|
|
392
|
+
"""
|
|
393
|
+
Register delete endpoint.
|
|
394
|
+
"""
|
|
395
|
+
|
|
326
396
|
@self.router.delete(
|
|
327
397
|
self.path_retrieve,
|
|
328
398
|
auth=self.delete_view_auth(),
|
|
@@ -338,35 +408,8 @@ class APIViewSet:
|
|
|
338
408
|
|
|
339
409
|
def views(self):
|
|
340
410
|
"""
|
|
341
|
-
Override
|
|
342
|
-
|
|
343
|
-
async def some_method(request, *args, **kwargs):
|
|
344
|
-
pass
|
|
345
|
-
|
|
346
|
-
You can add multilple views just doing:
|
|
347
|
-
|
|
348
|
-
@self.router.get(some_path, response=some_schema)
|
|
349
|
-
async def some_method(request, *args, **kwargs):
|
|
350
|
-
pass
|
|
351
|
-
|
|
352
|
-
@self.router.post(some_path, response=some_schema)
|
|
353
|
-
async def some_method(request, *args, **kwargs):
|
|
354
|
-
pass
|
|
355
|
-
|
|
356
|
-
If you provided a list of auths you can chose which of your views
|
|
357
|
-
should be authenticated:
|
|
358
|
-
|
|
359
|
-
AUTHENTICATED VIEW:
|
|
360
|
-
|
|
361
|
-
@self.router.get(some_path, response=some_schema, auth=self.auth)
|
|
362
|
-
async def some_method(request, *args, **kwargs):
|
|
363
|
-
pass
|
|
364
|
-
|
|
365
|
-
NOT AUTHENTICATED VIEW:
|
|
366
|
-
|
|
367
|
-
@self.router.post(some_path, response=some_schema)
|
|
368
|
-
async def some_method(request, *args, **kwargs):
|
|
369
|
-
pass
|
|
411
|
+
Override to register custom non-CRUD endpoints on self.router.
|
|
412
|
+
Use auth=self.auth or verb specific auth attributes if needed.
|
|
370
413
|
"""
|
|
371
414
|
|
|
372
415
|
async def _check_m2m_objs(
|
|
@@ -377,6 +420,10 @@ class APIViewSet:
|
|
|
377
420
|
related_manager: QuerySet,
|
|
378
421
|
remove: bool = False,
|
|
379
422
|
):
|
|
423
|
+
"""
|
|
424
|
+
Validate requested add/remove pk list for M2M operations.
|
|
425
|
+
Returns (errors, details, objects_to_process).
|
|
426
|
+
"""
|
|
380
427
|
errors, objs_detail, objs = [], [], []
|
|
381
428
|
rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
|
|
382
429
|
rel_model_name = model._meta.verbose_name.capitalize()
|
|
@@ -389,7 +436,7 @@ class APIViewSet:
|
|
|
389
436
|
continue
|
|
390
437
|
if remove ^ (rel_obj in rel_objs):
|
|
391
438
|
errors.append(
|
|
392
|
-
f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''}
|
|
439
|
+
f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''}in {self.model_util.model_name}"
|
|
393
440
|
)
|
|
394
441
|
continue
|
|
395
442
|
objs.append(rel_obj)
|
|
@@ -398,123 +445,144 @@ class APIViewSet:
|
|
|
398
445
|
)
|
|
399
446
|
return errors, objs_detail, objs
|
|
400
447
|
|
|
401
|
-
def _m2m_views(self):
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
448
|
+
def _m2m_views(self, m2m_relation: M2MRelationSchema):
|
|
449
|
+
"""
|
|
450
|
+
Register M2M get/manage endpoints for each relation in m2m_relations.
|
|
451
|
+
Supports optional per-relation filters and custom query handler:
|
|
452
|
+
<related_name>_query_params_handler.
|
|
453
|
+
"""
|
|
454
|
+
model = m2m_relation.model
|
|
455
|
+
related_name = m2m_relation.related_name
|
|
456
|
+
m2m_auth = m2m_relation.auth or self.m2m_auth
|
|
457
|
+
rel_util = ModelUtil(model)
|
|
458
|
+
rel_path = (
|
|
459
|
+
rel_util.verbose_name_path_resolver()
|
|
460
|
+
if not m2m_relation.path
|
|
461
|
+
else m2m_relation.path
|
|
462
|
+
)
|
|
463
|
+
m2m_add = m2m_relation.add
|
|
464
|
+
m2m_remove = m2m_relation.remove
|
|
465
|
+
m2m_get = m2m_relation.get
|
|
466
|
+
filters_schema = self.m2m_filters_schemas.get(related_name)
|
|
467
|
+
if m2m_get:
|
|
468
|
+
|
|
469
|
+
@self.router.get(
|
|
470
|
+
f"{self.path_retrieve}{rel_path}",
|
|
471
|
+
response={
|
|
472
|
+
200: List[model.generate_related_s(),],
|
|
473
|
+
self.error_codes: GenericMessageSchema,
|
|
474
|
+
},
|
|
475
|
+
auth=m2m_auth,
|
|
476
|
+
summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
477
|
+
description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
416
478
|
)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
related_qs = related_manager.all()
|
|
435
|
-
related_objs = [
|
|
436
|
-
await rel_util.read_s(
|
|
437
|
-
request, rel_obj, model.generate_related_s()
|
|
479
|
+
@unique_view(f"get_{self.model_util.model_name}_{rel_path}")
|
|
480
|
+
@paginate(self.pagination_class)
|
|
481
|
+
async def get_related(
|
|
482
|
+
request: HttpRequest,
|
|
483
|
+
pk: Path[self.path_schema], # type: ignore
|
|
484
|
+
filters: Query[filters_schema] = None, # type: ignore
|
|
485
|
+
):
|
|
486
|
+
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
487
|
+
related_manager = getattr(obj, related_name)
|
|
488
|
+
related_qs = related_manager.all()
|
|
489
|
+
if (
|
|
490
|
+
filters is not None
|
|
491
|
+
and (
|
|
492
|
+
query_handler := getattr(
|
|
493
|
+
self,
|
|
494
|
+
f"{m2m_relation.related_name}_query_params_handler",
|
|
495
|
+
None,
|
|
438
496
|
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return related_objs
|
|
442
|
-
|
|
443
|
-
if self.m2m_add or self.m2m_remove:
|
|
444
|
-
summary = f"{'Add or Remove' if self.m2m_add and self.m2m_remove else 'Add' if self.m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
445
|
-
description = f"{'Add or remove' if self.m2m_add and self.m2m_remove else 'Add' if self.m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
446
|
-
schema_in = (
|
|
447
|
-
M2MSchemaIn
|
|
448
|
-
if self.m2m_add and self.m2m_remove
|
|
449
|
-
else M2MAddSchemaIn
|
|
450
|
-
if self.m2m_add
|
|
451
|
-
else M2MRemoveSchemaIn
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
@self.router.post(
|
|
455
|
-
f"{self.path_retrieve}{rel_path}/",
|
|
456
|
-
response={
|
|
457
|
-
200: M2MSchemaOut,
|
|
458
|
-
self.error_codes: GenericMessageSchema,
|
|
459
|
-
},
|
|
460
|
-
auth=m2m_auth,
|
|
461
|
-
summary=summary,
|
|
462
|
-
description=description,
|
|
463
|
-
)
|
|
464
|
-
@unique_view(f"manage_{self.model_util.model_name}_{rel_path}")
|
|
465
|
-
async def manage_related(
|
|
466
|
-
request: HttpRequest,
|
|
467
|
-
pk: Path[self.path_schema], # type: ignore
|
|
468
|
-
data: schema_in, # type: ignore
|
|
497
|
+
)
|
|
498
|
+
is not None
|
|
469
499
|
):
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
remove_objs,
|
|
488
|
-
) = await self._check_m2m_objs(
|
|
489
|
-
request,
|
|
490
|
-
data.remove,
|
|
491
|
-
model,
|
|
492
|
-
related_manager,
|
|
493
|
-
remove=True,
|
|
494
|
-
)
|
|
500
|
+
related_qs = await query_handler(related_qs, filters.model_dump())
|
|
501
|
+
related_objs = [
|
|
502
|
+
await rel_util.read_s(request, rel_obj, model.generate_related_s())
|
|
503
|
+
async for rel_obj in related_qs
|
|
504
|
+
]
|
|
505
|
+
return related_objs
|
|
506
|
+
|
|
507
|
+
if m2m_add or m2m_remove:
|
|
508
|
+
summary = f"{'Add or Remove' if m2m_add and m2m_remove else 'Add' if m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
509
|
+
description = f"{'Add or remove' if m2m_add and m2m_remove else 'Add' if m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
510
|
+
schema_in = (
|
|
511
|
+
M2MSchemaIn
|
|
512
|
+
if m2m_add and m2m_remove
|
|
513
|
+
else M2MAddSchemaIn
|
|
514
|
+
if m2m_add
|
|
515
|
+
else M2MRemoveSchemaIn
|
|
516
|
+
)
|
|
495
517
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
518
|
+
@self.router.post(
|
|
519
|
+
f"{self.path_retrieve}{rel_path}/",
|
|
520
|
+
response={
|
|
521
|
+
200: M2MSchemaOut,
|
|
522
|
+
self.error_codes: GenericMessageSchema,
|
|
523
|
+
},
|
|
524
|
+
auth=m2m_auth,
|
|
525
|
+
summary=summary,
|
|
526
|
+
description=description,
|
|
527
|
+
)
|
|
528
|
+
@unique_view(f"manage_{self.model_util.model_name}_{rel_path}")
|
|
529
|
+
async def manage_related(
|
|
530
|
+
request: HttpRequest,
|
|
531
|
+
pk: Path[self.path_schema], # type: ignore
|
|
532
|
+
data: schema_in, # type: ignore
|
|
533
|
+
):
|
|
534
|
+
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
535
|
+
related_manager: QuerySet = getattr(obj, related_name)
|
|
536
|
+
add_errors, add_details, add_objs = [], [], []
|
|
537
|
+
remove_errors, remove_details, remove_objs = [], [], []
|
|
538
|
+
|
|
539
|
+
if m2m_add and hasattr(data, "add"):
|
|
540
|
+
(
|
|
541
|
+
add_errors,
|
|
542
|
+
add_details,
|
|
543
|
+
add_objs,
|
|
544
|
+
) = await self._check_m2m_objs(
|
|
545
|
+
request, data.add, model, related_manager
|
|
546
|
+
)
|
|
547
|
+
if m2m_remove and hasattr(data, "remove"):
|
|
548
|
+
(
|
|
549
|
+
remove_errors,
|
|
550
|
+
remove_details,
|
|
551
|
+
remove_objs,
|
|
552
|
+
) = await self._check_m2m_objs(
|
|
553
|
+
request,
|
|
554
|
+
data.remove,
|
|
555
|
+
model,
|
|
556
|
+
related_manager,
|
|
557
|
+
remove=True,
|
|
499
558
|
)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
559
|
+
await asyncio.gather(
|
|
560
|
+
related_manager.aadd(*add_objs),
|
|
561
|
+
related_manager.aremove(*remove_objs),
|
|
562
|
+
)
|
|
563
|
+
results = add_details + remove_details
|
|
564
|
+
errors = add_errors + remove_errors
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
"results": {
|
|
568
|
+
"count": len(results),
|
|
569
|
+
"details": results,
|
|
570
|
+
},
|
|
571
|
+
"errors": {
|
|
572
|
+
"count": len(errors),
|
|
573
|
+
"details": errors,
|
|
574
|
+
},
|
|
575
|
+
}
|
|
513
576
|
|
|
514
577
|
def _add_views(self):
|
|
578
|
+
"""
|
|
579
|
+
Register CRUD (unless disabled), custom views, and M2M endpoints.
|
|
580
|
+
If 'all' in disable only CRUD is skipped; M2M + custom still added.
|
|
581
|
+
"""
|
|
515
582
|
if "all" in self.disable:
|
|
516
583
|
if self.m2m_relations:
|
|
517
|
-
self.
|
|
584
|
+
for m2m_relation in self.m2m_relations:
|
|
585
|
+
self._m2m_views(m2m_relation)
|
|
518
586
|
self.views()
|
|
519
587
|
return self.router
|
|
520
588
|
|
|
@@ -526,8 +594,12 @@ class APIViewSet:
|
|
|
526
594
|
|
|
527
595
|
self.views()
|
|
528
596
|
if self.m2m_relations:
|
|
529
|
-
self.
|
|
597
|
+
for m2m_relation in self.m2m_relations:
|
|
598
|
+
self._m2m_views(m2m_relation)
|
|
530
599
|
return self.router
|
|
531
600
|
|
|
532
601
|
def add_views_to_route(self):
|
|
602
|
+
"""
|
|
603
|
+
Attach router with registered endpoints to the NinjaAPI instance.
|
|
604
|
+
"""
|
|
533
605
|
return self.api.add_router(f"{self.api_route_path}", self._add_views())
|