django-ninja-aio-crud 0.11.3__py3-none-any.whl → 1.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.
- {django_ninja_aio_crud-0.11.3.dist-info → django_ninja_aio_crud-1.0.0.dist-info}/METADATA +1 -1
- {django_ninja_aio_crud-0.11.3.dist-info → django_ninja_aio_crud-1.0.0.dist-info}/RECORD +7 -7
- ninja_aio/__init__.py +1 -1
- ninja_aio/schemas.py +40 -2
- ninja_aio/views.py +188 -106
- {django_ninja_aio_crud-0.11.3.dist-info → django_ninja_aio_crud-1.0.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-0.11.3.dist-info → django_ninja_aio_crud-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=0C1TJ30_2xcqtKhHchk1gb1IzGrxY2u1ufIO-lIRgRM,119
|
|
2
2
|
ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
|
|
3
3
|
ninja_aio/auth.py,sha256=zUwruKcz7MXuOnWp5k1CCSwEc8s2Lyqqk7Qm9kPbJ3o,5149
|
|
4
4
|
ninja_aio/decorators.py,sha256=LsvHbMxmw_So8NV0ey5NRRvSbfYkOZLeLQ4Fix7rQAY,5519
|
|
@@ -6,10 +6,10 @@ ninja_aio/exceptions.py,sha256=iEX4PNqtRXXr75M8veOynmFZcIE5lGURHU_ISSgzX0Y,2578
|
|
|
6
6
|
ninja_aio/models.py,sha256=-p1wgOg-r5bYWQ9DzUSNKxsUvWiDg6kruMQ_LxZFvQE,32948
|
|
7
7
|
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
8
8
|
ninja_aio/renders.py,sha256=0eYklRKd59aV4cZDom5vLZyA99Ob17OwkpMybsRXvyg,1970
|
|
9
|
-
ninja_aio/schemas.py,sha256=
|
|
9
|
+
ninja_aio/schemas.py,sha256=iv3VHCMlzez6Qs70zITYIwEz0EFOOaMPDVGRcTZCygA,1875
|
|
10
10
|
ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
|
|
11
|
-
ninja_aio/views.py,sha256=
|
|
12
|
-
django_ninja_aio_crud-0.
|
|
13
|
-
django_ninja_aio_crud-0.
|
|
14
|
-
django_ninja_aio_crud-0.
|
|
15
|
-
django_ninja_aio_crud-0.
|
|
11
|
+
ninja_aio/views.py,sha256=7sv8tpRp8CJC2sxQr-jmbjyjcJe7DabYPf8F7nieZ4Y,23518
|
|
12
|
+
django_ninja_aio_crud-1.0.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
13
|
+
django_ninja_aio_crud-1.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
14
|
+
django_ninja_aio_crud-1.0.0.dist-info/METADATA,sha256=aV0vq6ijySGbggzrwNJ1mCC7HG_6fo1jeF9Ybb_j9Mk,14216
|
|
15
|
+
django_ninja_aio_crud-1.0.0.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
ninja_aio/schemas.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
from typing import Optional, Type
|
|
2
|
+
|
|
1
3
|
from ninja import Schema
|
|
2
|
-
from
|
|
4
|
+
from .models import ModelSerializer
|
|
5
|
+
from django.db.models import Model
|
|
6
|
+
from pydantic import BaseModel, RootModel, ConfigDict
|
|
3
7
|
|
|
4
8
|
|
|
5
9
|
class GenericMessageSchema(RootModel[dict[str, str]]):
|
|
@@ -26,4 +30,38 @@ class M2MRemoveSchemaIn(Schema):
|
|
|
26
30
|
|
|
27
31
|
class M2MSchemaIn(Schema):
|
|
28
32
|
add: list = []
|
|
29
|
-
remove: list = []
|
|
33
|
+
remove: list = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class M2MRelationSchema(BaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Configuration schema for declaring a Many-to-Many relation in the API.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
model (Type[ModelSerializer] | Type[Model]): Target model class or its serializer.
|
|
42
|
+
related_name (str): Name of the relationship field on the Django model.
|
|
43
|
+
add (bool): Enable adding related objects (default True).
|
|
44
|
+
remove (bool): Enable removing related objects (default True).
|
|
45
|
+
get (bool): Enable retrieving related objects (default True).
|
|
46
|
+
path (str | None): Optional custom URL path segment (None/"" => auto-generated).
|
|
47
|
+
auth (list | None): Optional list of authentication backends for the endpoints.
|
|
48
|
+
filters (dict[str, tuple] | None): Field name -> (type, default) pairs for query filtering.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
M2MRelationSchema(
|
|
52
|
+
model=BookSerializer,
|
|
53
|
+
related_name="authors",
|
|
54
|
+
filters={"country": ("str", '')}
|
|
55
|
+
)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
model: Type[ModelSerializer] | Type[Model]
|
|
59
|
+
related_name: str
|
|
60
|
+
add: bool = True
|
|
61
|
+
remove: bool = True
|
|
62
|
+
get: bool = True
|
|
63
|
+
path: Optional[str] = ""
|
|
64
|
+
auth: Optional[list] = None
|
|
65
|
+
filters: Optional[dict[str, tuple]] = None
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
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,60 +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** (`tuple[ModelSerializer | Model, str]`): Many-to-many relations to manage.
|
|
105
|
-
- **m2m_add** (`bool`): Enable add operation for M2M relations.
|
|
106
|
-
- **m2m_remove** (`bool`): Enable remove operation for M2M relations.
|
|
107
|
-
- **m2m_get** (`bool`): Enable get operation for M2M relations.
|
|
108
|
-
- **m2m_auth** (`list | None`): Authentication for M2M views.
|
|
109
|
-
|
|
110
|
-
## Notes:
|
|
111
|
-
If the model is a ModelSerializer instance, schemas are generated
|
|
112
|
-
automatically based on Create, Read, and Update serializers.
|
|
113
|
-
Override the `views` method to add custom views.
|
|
114
|
-
Override the `query_params_handler` method to handle query params
|
|
115
|
-
and return a filtered queryset.
|
|
116
|
-
|
|
117
|
-
## Methods:
|
|
118
|
-
- **create_view**: Creates a new object.
|
|
119
|
-
- **list_view**: Lists all objects.
|
|
120
|
-
- **retrieve_view**: Retrieves an object by its primary key.
|
|
121
|
-
- **update_view**: Updates an object by its primary key.
|
|
122
|
-
- **delete_view**: Deletes an object by its primary key.
|
|
123
|
-
- **views**: Override to add custom views.
|
|
124
|
-
- **add_views_to_route**: Adds the views to the API route.
|
|
125
|
-
|
|
126
|
-
## Example:
|
|
127
|
-
class MyModelViewSet(APIViewSet):
|
|
128
|
-
model = MyModel # Your Django model
|
|
129
|
-
api = my_api_instance # Your NinjaAPI instance
|
|
79
|
+
Base viewset generating async CRUD + optional M2M endpoints for a Django model.
|
|
130
80
|
|
|
81
|
+
Usage:
|
|
82
|
+
class MyModelViewSet(APIViewSet):
|
|
83
|
+
model = MyModel
|
|
84
|
+
api = api
|
|
131
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.
|
|
132
159
|
"""
|
|
133
160
|
|
|
134
161
|
model: ModelSerializer | Model
|
|
@@ -150,12 +177,8 @@ class APIViewSet:
|
|
|
150
177
|
retrieve_docs = "Retrieve a specific object by its primary key."
|
|
151
178
|
update_docs = "Update an object by its primary key."
|
|
152
179
|
delete_docs = "Delete an object by its primary key."
|
|
153
|
-
m2m_relations: list[
|
|
154
|
-
m2m_add = True
|
|
155
|
-
m2m_remove = True
|
|
156
|
-
m2m_get = True
|
|
180
|
+
m2m_relations: list[M2MRelationSchema] = []
|
|
157
181
|
m2m_auth: list | None = NOT_SET
|
|
158
|
-
m2m_path: str = ""
|
|
159
182
|
|
|
160
183
|
def __init__(self) -> None:
|
|
161
184
|
self.error_codes = ERROR_CODES
|
|
@@ -163,6 +186,7 @@ class APIViewSet:
|
|
|
163
186
|
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
164
187
|
self.path_schema = self._generate_path_schema()
|
|
165
188
|
self.filters_schema = self._generate_filters_schema()
|
|
189
|
+
self.m2m_filters_schemas = self._generate_m2m_filters_schemas()
|
|
166
190
|
self.model_verbose_name = self.model._meta.verbose_name.capitalize()
|
|
167
191
|
self.router_tag = self.model_verbose_name
|
|
168
192
|
self.router = Router(tags=[self.router_tag])
|
|
@@ -177,8 +201,7 @@ class APIViewSet:
|
|
|
177
201
|
@property
|
|
178
202
|
def _crud_views(self):
|
|
179
203
|
"""
|
|
180
|
-
|
|
181
|
-
value: tuple with schema and view method
|
|
204
|
+
Mapping of CRUD operation name to (response schema, view factory).
|
|
182
205
|
"""
|
|
183
206
|
return {
|
|
184
207
|
"create": (self.schema_in, self.create_view),
|
|
@@ -189,6 +212,9 @@ class APIViewSet:
|
|
|
189
212
|
}
|
|
190
213
|
|
|
191
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
|
+
"""
|
|
192
218
|
auth = getattr(self, f"{view_type}_auth", None)
|
|
193
219
|
return auth if auth is not NOT_SET else self.auth
|
|
194
220
|
|
|
@@ -205,20 +231,47 @@ class APIViewSet:
|
|
|
205
231
|
return self._auth_view("delete")
|
|
206
232
|
|
|
207
233
|
def _generate_schema(self, fields: dict, name: str) -> Schema:
|
|
234
|
+
"""
|
|
235
|
+
Dynamically build a Pydantic model for path / filter schemas.
|
|
236
|
+
"""
|
|
208
237
|
return create_model(f"{self.model_util.model_name}{name}", **fields)
|
|
209
238
|
|
|
210
239
|
def _generate_path_schema(self):
|
|
240
|
+
"""
|
|
241
|
+
Schema containing only the primary key field for path resolution.
|
|
242
|
+
"""
|
|
211
243
|
return self._generate_schema(
|
|
212
244
|
{self.model_util.model_pk_name: (int | str, ...)}, "PathSchema"
|
|
213
245
|
)
|
|
214
246
|
|
|
215
247
|
def _generate_filters_schema(self):
|
|
248
|
+
"""
|
|
249
|
+
Build filters schema from query_params definition.
|
|
250
|
+
"""
|
|
216
251
|
return self._generate_schema(self.query_params, "FiltersSchema")
|
|
217
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
|
+
|
|
218
265
|
def _get_pk(self, data: Schema):
|
|
266
|
+
"""
|
|
267
|
+
Extract pk from a path schema instance.
|
|
268
|
+
"""
|
|
219
269
|
return data.model_dump()[self.model_util.model_pk_name]
|
|
220
270
|
|
|
221
271
|
def get_schemas(self):
|
|
272
|
+
"""
|
|
273
|
+
Return (schema_out, schema_in, schema_update), generating them if model is a ModelSerializer.
|
|
274
|
+
"""
|
|
222
275
|
if isinstance(self.model, ModelSerializerMeta):
|
|
223
276
|
return (
|
|
224
277
|
self.model.generate_read_s(),
|
|
@@ -231,13 +284,16 @@ class APIViewSet:
|
|
|
231
284
|
self, queryset: QuerySet[ModelSerializer], filters: dict
|
|
232
285
|
):
|
|
233
286
|
"""
|
|
234
|
-
Override
|
|
235
|
-
|
|
236
|
-
|
|
287
|
+
Override to apply custom filtering logic for list_view.
|
|
288
|
+
filters is already validated and dumped.
|
|
289
|
+
Return the (possibly modified) queryset.
|
|
237
290
|
"""
|
|
238
291
|
return queryset
|
|
239
292
|
|
|
240
293
|
def create_view(self):
|
|
294
|
+
"""
|
|
295
|
+
Register create endpoint.
|
|
296
|
+
"""
|
|
241
297
|
@self.router.post(
|
|
242
298
|
self.path,
|
|
243
299
|
auth=self.post_view_auth(),
|
|
@@ -252,6 +308,9 @@ class APIViewSet:
|
|
|
252
308
|
return create
|
|
253
309
|
|
|
254
310
|
def list_view(self):
|
|
311
|
+
"""
|
|
312
|
+
Register list endpoint with pagination and optional filters.
|
|
313
|
+
"""
|
|
255
314
|
@self.router.get(
|
|
256
315
|
self.get_path,
|
|
257
316
|
auth=self.get_view_auth(),
|
|
@@ -285,6 +344,9 @@ class APIViewSet:
|
|
|
285
344
|
return list
|
|
286
345
|
|
|
287
346
|
def retrieve_view(self):
|
|
347
|
+
"""
|
|
348
|
+
Register retrieve endpoint.
|
|
349
|
+
"""
|
|
288
350
|
@self.router.get(
|
|
289
351
|
self.get_path_retrieve,
|
|
290
352
|
auth=self.get_view_auth(),
|
|
@@ -300,6 +362,9 @@ class APIViewSet:
|
|
|
300
362
|
return retrieve
|
|
301
363
|
|
|
302
364
|
def update_view(self):
|
|
365
|
+
"""
|
|
366
|
+
Register update endpoint.
|
|
367
|
+
"""
|
|
303
368
|
@self.router.patch(
|
|
304
369
|
self.path_retrieve,
|
|
305
370
|
auth=self.patch_view_auth(),
|
|
@@ -320,6 +385,9 @@ class APIViewSet:
|
|
|
320
385
|
return update
|
|
321
386
|
|
|
322
387
|
def delete_view(self):
|
|
388
|
+
"""
|
|
389
|
+
Register delete endpoint.
|
|
390
|
+
"""
|
|
323
391
|
@self.router.delete(
|
|
324
392
|
self.path_retrieve,
|
|
325
393
|
auth=self.delete_view_auth(),
|
|
@@ -335,35 +403,8 @@ class APIViewSet:
|
|
|
335
403
|
|
|
336
404
|
def views(self):
|
|
337
405
|
"""
|
|
338
|
-
Override
|
|
339
|
-
|
|
340
|
-
async def some_method(request, *args, **kwargs):
|
|
341
|
-
pass
|
|
342
|
-
|
|
343
|
-
You can add multilple views just doing:
|
|
344
|
-
|
|
345
|
-
@self.router.get(some_path, response=some_schema)
|
|
346
|
-
async def some_method(request, *args, **kwargs):
|
|
347
|
-
pass
|
|
348
|
-
|
|
349
|
-
@self.router.post(some_path, response=some_schema)
|
|
350
|
-
async def some_method(request, *args, **kwargs):
|
|
351
|
-
pass
|
|
352
|
-
|
|
353
|
-
If you provided a list of auths you can chose which of your views
|
|
354
|
-
should be authenticated:
|
|
355
|
-
|
|
356
|
-
AUTHENTICATED VIEW:
|
|
357
|
-
|
|
358
|
-
@self.router.get(some_path, response=some_schema, auth=self.auth)
|
|
359
|
-
async def some_method(request, *args, **kwargs):
|
|
360
|
-
pass
|
|
361
|
-
|
|
362
|
-
NOT AUTHENTICATED VIEW:
|
|
363
|
-
|
|
364
|
-
@self.router.post(some_path, response=some_schema)
|
|
365
|
-
async def some_method(request, *args, **kwargs):
|
|
366
|
-
pass
|
|
406
|
+
Override to register custom non-CRUD endpoints on self.router.
|
|
407
|
+
Use auth=self.auth or verb specific auth attributes if needed.
|
|
367
408
|
"""
|
|
368
409
|
|
|
369
410
|
async def _check_m2m_objs(
|
|
@@ -374,6 +415,10 @@ class APIViewSet:
|
|
|
374
415
|
related_manager: QuerySet,
|
|
375
416
|
remove: bool = False,
|
|
376
417
|
):
|
|
418
|
+
"""
|
|
419
|
+
Validate requested add/remove pk list for M2M operations.
|
|
420
|
+
Returns (errors, details, objects_to_process).
|
|
421
|
+
"""
|
|
377
422
|
errors, objs_detail, objs = [], [], []
|
|
378
423
|
rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
|
|
379
424
|
rel_model_name = model._meta.verbose_name.capitalize()
|
|
@@ -386,7 +431,7 @@ class APIViewSet:
|
|
|
386
431
|
continue
|
|
387
432
|
if remove ^ (rel_obj in rel_objs):
|
|
388
433
|
errors.append(
|
|
389
|
-
f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''}
|
|
434
|
+
f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''}in {self.model_util.model_name}"
|
|
390
435
|
)
|
|
391
436
|
continue
|
|
392
437
|
objs.append(rel_obj)
|
|
@@ -396,14 +441,26 @@ class APIViewSet:
|
|
|
396
441
|
return errors, objs_detail, objs
|
|
397
442
|
|
|
398
443
|
def _m2m_views(self):
|
|
399
|
-
|
|
444
|
+
"""
|
|
445
|
+
Register M2M get/manage endpoints for each relation in m2m_relations.
|
|
446
|
+
Supports optional per-relation filters and custom query handler:
|
|
447
|
+
<related_name>_query_params_handler.
|
|
448
|
+
"""
|
|
449
|
+
for m2m_data in self.m2m_relations:
|
|
450
|
+
model = m2m_data.model
|
|
451
|
+
related_name = m2m_data.related_name
|
|
452
|
+
m2m_auth = m2m_data.auth or self.m2m_auth
|
|
400
453
|
rel_util = ModelUtil(model)
|
|
401
454
|
rel_path = (
|
|
402
455
|
rel_util.verbose_name_path_resolver()
|
|
403
|
-
if not
|
|
404
|
-
else
|
|
456
|
+
if not m2m_data.path
|
|
457
|
+
else m2m_data.path
|
|
405
458
|
)
|
|
406
|
-
|
|
459
|
+
m2m_add = m2m_data.add
|
|
460
|
+
m2m_remove = m2m_data.remove
|
|
461
|
+
m2m_get = m2m_data.get
|
|
462
|
+
filters_schema = self.m2m_filters_schemas.get(related_name)
|
|
463
|
+
if m2m_get:
|
|
407
464
|
|
|
408
465
|
@self.router.get(
|
|
409
466
|
f"{self.path_retrieve}{rel_path}",
|
|
@@ -411,16 +468,34 @@ class APIViewSet:
|
|
|
411
468
|
200: List[model.generate_related_s(),],
|
|
412
469
|
self.error_codes: GenericMessageSchema,
|
|
413
470
|
},
|
|
414
|
-
auth=
|
|
471
|
+
auth=m2m_auth,
|
|
415
472
|
summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
416
473
|
description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
417
474
|
)
|
|
418
475
|
@unique_view(f"get_{self.model_util.model_name}_{rel_path}")
|
|
419
476
|
@paginate(self.pagination_class)
|
|
420
|
-
async def get_related(
|
|
477
|
+
async def get_related(
|
|
478
|
+
request: HttpRequest,
|
|
479
|
+
pk: Path[self.path_schema], # type: ignore
|
|
480
|
+
filters: Query[filters_schema] = None # type: ignore
|
|
481
|
+
):
|
|
421
482
|
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
422
483
|
related_manager = getattr(obj, related_name)
|
|
423
484
|
related_qs = related_manager.all()
|
|
485
|
+
if (
|
|
486
|
+
filters is not None
|
|
487
|
+
and (
|
|
488
|
+
query_handler := getattr(
|
|
489
|
+
self,
|
|
490
|
+
f"{m2m_data.related_name}_query_params_handler",
|
|
491
|
+
None,
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
is not None
|
|
495
|
+
):
|
|
496
|
+
related_qs = await query_handler(
|
|
497
|
+
related_qs, filters.model_dump()
|
|
498
|
+
)
|
|
424
499
|
related_objs = [
|
|
425
500
|
await rel_util.read_s(
|
|
426
501
|
request, rel_obj, model.generate_related_s()
|
|
@@ -429,14 +504,14 @@ class APIViewSet:
|
|
|
429
504
|
]
|
|
430
505
|
return related_objs
|
|
431
506
|
|
|
432
|
-
if
|
|
433
|
-
summary = f"{'Add or Remove' if
|
|
434
|
-
description = f"{'Add or remove' if
|
|
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()}"
|
|
435
510
|
schema_in = (
|
|
436
511
|
M2MSchemaIn
|
|
437
|
-
if
|
|
512
|
+
if m2m_add and m2m_remove
|
|
438
513
|
else M2MAddSchemaIn
|
|
439
|
-
if
|
|
514
|
+
if m2m_add
|
|
440
515
|
else M2MRemoveSchemaIn
|
|
441
516
|
)
|
|
442
517
|
|
|
@@ -446,7 +521,7 @@ class APIViewSet:
|
|
|
446
521
|
200: M2MSchemaOut,
|
|
447
522
|
self.error_codes: GenericMessageSchema,
|
|
448
523
|
},
|
|
449
|
-
auth=
|
|
524
|
+
auth=m2m_auth,
|
|
450
525
|
summary=summary,
|
|
451
526
|
description=description,
|
|
452
527
|
)
|
|
@@ -461,7 +536,7 @@ class APIViewSet:
|
|
|
461
536
|
add_errors, add_details, add_objs = [], [], []
|
|
462
537
|
remove_errors, remove_details, remove_objs = [], [], []
|
|
463
538
|
|
|
464
|
-
if
|
|
539
|
+
if m2m_add and hasattr(data, "add"):
|
|
465
540
|
(
|
|
466
541
|
add_errors,
|
|
467
542
|
add_details,
|
|
@@ -469,7 +544,7 @@ class APIViewSet:
|
|
|
469
544
|
) = await self._check_m2m_objs(
|
|
470
545
|
request, data.add, model, related_manager
|
|
471
546
|
)
|
|
472
|
-
if
|
|
547
|
+
if m2m_remove and hasattr(data, "remove"):
|
|
473
548
|
(
|
|
474
549
|
remove_errors,
|
|
475
550
|
remove_details,
|
|
@@ -501,6 +576,10 @@ class APIViewSet:
|
|
|
501
576
|
}
|
|
502
577
|
|
|
503
578
|
def _add_views(self):
|
|
579
|
+
"""
|
|
580
|
+
Register CRUD (unless disabled), custom views, and M2M endpoints.
|
|
581
|
+
If 'all' in disable only CRUD is skipped; M2M + custom still added.
|
|
582
|
+
"""
|
|
504
583
|
if "all" in self.disable:
|
|
505
584
|
if self.m2m_relations:
|
|
506
585
|
self._m2m_views()
|
|
@@ -519,4 +598,7 @@ class APIViewSet:
|
|
|
519
598
|
return self.router
|
|
520
599
|
|
|
521
600
|
def add_views_to_route(self):
|
|
601
|
+
"""
|
|
602
|
+
Attach router with registered endpoints to the NinjaAPI instance.
|
|
603
|
+
"""
|
|
522
604
|
return self.api.add_router(f"{self.api_route_path}", self._add_views())
|
|
File without changes
|
{django_ninja_aio_crud-0.11.3.dist-info → django_ninja_aio_crud-1.0.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|