django-ninja-aio-crud 0.10.2__py3-none-any.whl → 2.4.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-2.4.0.dist-info/METADATA +382 -0
- django_ninja_aio_crud-2.4.0.dist-info/RECORD +29 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/api.py +24 -2
- ninja_aio/auth.py +186 -4
- ninja_aio/decorators/__init__.py +23 -0
- ninja_aio/decorators/operations.py +9 -0
- ninja_aio/decorators/views.py +219 -0
- ninja_aio/exceptions.py +36 -1
- ninja_aio/factory/__init__.py +3 -0
- ninja_aio/factory/operations.py +296 -0
- ninja_aio/helpers/__init__.py +0 -0
- ninja_aio/helpers/api.py +506 -0
- ninja_aio/helpers/query.py +108 -0
- ninja_aio/models/__init__.py +4 -0
- ninja_aio/models/serializers.py +738 -0
- ninja_aio/models/utils.py +894 -0
- ninja_aio/renders.py +26 -26
- ninja_aio/schemas/__init__.py +23 -0
- ninja_aio/{schemas.py → schemas/api.py} +0 -5
- ninja_aio/schemas/generics.py +5 -0
- ninja_aio/schemas/helpers.py +170 -0
- ninja_aio/types.py +3 -1
- ninja_aio/views/__init__.py +3 -0
- ninja_aio/views/api.py +582 -0
- ninja_aio/views/mixins.py +275 -0
- django_ninja_aio_crud-0.10.2.dist-info/METADATA +0 -526
- django_ninja_aio_crud-0.10.2.dist-info/RECORD +0 -14
- ninja_aio/models.py +0 -549
- ninja_aio/views.py +0 -522
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/licenses/LICENSE +0 -0
ninja_aio/views/api.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from ninja import NinjaAPI, Router, Schema, Path, Query
|
|
4
|
+
from ninja.constants import NOT_SET
|
|
5
|
+
from ninja.pagination import paginate, AsyncPaginationBase, PageNumberPagination
|
|
6
|
+
from django.http import HttpRequest
|
|
7
|
+
from django.db.models import Model, QuerySet
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from pydantic import create_model
|
|
10
|
+
|
|
11
|
+
from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema, DecoratorsSchema
|
|
12
|
+
|
|
13
|
+
from ninja_aio.models import ModelSerializer, ModelUtil
|
|
14
|
+
from ninja_aio.schemas import (
|
|
15
|
+
GenericMessageSchema,
|
|
16
|
+
M2MRelationSchema,
|
|
17
|
+
)
|
|
18
|
+
from ninja_aio.helpers.api import ManyToManyAPI
|
|
19
|
+
from ninja_aio.types import ModelSerializerMeta, VIEW_TYPES
|
|
20
|
+
from ninja_aio.decorators import unique_view, decorate_view
|
|
21
|
+
from ninja_aio.models import serializers
|
|
22
|
+
|
|
23
|
+
ERROR_CODES = frozenset({400, 401, 404})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class API:
|
|
27
|
+
api: NinjaAPI = None
|
|
28
|
+
router_tag: str = ""
|
|
29
|
+
router_tags: list[str] = []
|
|
30
|
+
api_route_path: str = ""
|
|
31
|
+
auth: list | None = NOT_SET
|
|
32
|
+
router: Router = None
|
|
33
|
+
|
|
34
|
+
def views(self):
|
|
35
|
+
"""
|
|
36
|
+
Override this method to add your custom views. For example:
|
|
37
|
+
@self.router.get(some_path, response=some_schema)
|
|
38
|
+
async def some_method(request, *args, **kwargs):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
You can add views just doing:
|
|
42
|
+
|
|
43
|
+
@self.router.get(some_path, response=some_schema)
|
|
44
|
+
async def some_method(request, *args, **kwargs):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@self.router.post(some_path, response=some_schema)
|
|
48
|
+
async def some_method(request, *args, **kwargs):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
If you provided a list of auths you can chose which of your views
|
|
52
|
+
should be authenticated:
|
|
53
|
+
|
|
54
|
+
AUTHENTICATED VIEW:
|
|
55
|
+
|
|
56
|
+
@self.router.get(some_path, response=some_schema, auth=self.auth)
|
|
57
|
+
async def some_method(request, *args, **kwargs):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
NOT AUTHENTICATED VIEW:
|
|
61
|
+
|
|
62
|
+
@self.router.post(some_path, response=some_schema)
|
|
63
|
+
async def some_method(request, *args, **kwargs):
|
|
64
|
+
pass
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def _add_views(self):
|
|
69
|
+
for name in dir(self.__class__):
|
|
70
|
+
method = getattr(self.__class__, name)
|
|
71
|
+
if hasattr(method, "_api_register"):
|
|
72
|
+
method._api_register(self)
|
|
73
|
+
|
|
74
|
+
def add_views_to_route(self):
|
|
75
|
+
return self.api.add_router(f"{self.api_route_path}", self._add_views())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class APIView(API):
|
|
79
|
+
"""
|
|
80
|
+
Base class to register custom, non-CRUD endpoints on a Ninja Router.
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
from ninja_aio.decorations import api_get
|
|
84
|
+
|
|
85
|
+
@api.view(prefix="/custom", tags=["Custom"])
|
|
86
|
+
class CustomAPIView(APIView):
|
|
87
|
+
@api_get("/hello", response=SomeSchema)
|
|
88
|
+
async def hello(request):
|
|
89
|
+
return SomeSchema(...)
|
|
90
|
+
|
|
91
|
+
or
|
|
92
|
+
|
|
93
|
+
class CustomAPIView(APIView):
|
|
94
|
+
api = api
|
|
95
|
+
api_route_path = "/custom"
|
|
96
|
+
router_tags = ["Custom"]
|
|
97
|
+
|
|
98
|
+
def views(self):
|
|
99
|
+
@self.router.get("/hello", response=SomeSchema)
|
|
100
|
+
async def hello(request):
|
|
101
|
+
return SomeSchema(...)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
CustomAPIView().add_views_to_route()
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
api: NinjaAPI instance used to mount the router.
|
|
108
|
+
router_tag: Single tag used if router_tags is not provided.
|
|
109
|
+
router_tags: List of tags assigned to the router.
|
|
110
|
+
api_route_path: Base path where the router is mounted.
|
|
111
|
+
auth: Default auth list or NOT_SET for unauthenticated endpoints.
|
|
112
|
+
router: Router instance where views are registered.
|
|
113
|
+
error_codes: Common error codes returned by endpoints.
|
|
114
|
+
|
|
115
|
+
Overridable methods:
|
|
116
|
+
views(): Register your endpoints using self.router.get/post/patch/delete.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self, api: NinjaAPI = None, prefix: str = None, tags: list[str] = None
|
|
121
|
+
) -> None:
|
|
122
|
+
self.api = api or self.api
|
|
123
|
+
self.api_route_path = prefix or self.api_route_path
|
|
124
|
+
self.router_tags = tags or self.router_tags or [self.router_tag]
|
|
125
|
+
self.router = Router(tags=self.router_tags)
|
|
126
|
+
self.error_codes = ERROR_CODES
|
|
127
|
+
|
|
128
|
+
def _add_views(self):
|
|
129
|
+
super()._add_views()
|
|
130
|
+
self.views()
|
|
131
|
+
return self.router
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class APIViewSet(API):
|
|
135
|
+
"""
|
|
136
|
+
Base viewset generating async CRUD + optional M2M endpoints for a Django model.
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
@api.viewset(model=MyModel)
|
|
140
|
+
class MyModelViewSet(APIViewSet):
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
or
|
|
144
|
+
|
|
145
|
+
class MyModelViewSet(APIViewSet):
|
|
146
|
+
model = MyModel
|
|
147
|
+
api = api
|
|
148
|
+
MyModelViewSet().add_views_to_route()
|
|
149
|
+
|
|
150
|
+
Automatic schema generation:
|
|
151
|
+
If model is a ModelSerializer (subclass of ModelSerializerMeta),
|
|
152
|
+
read/create/update schemas are auto-generated from its serializers.
|
|
153
|
+
Otherwise provide schema_in / schema_out / schema_update manually.
|
|
154
|
+
|
|
155
|
+
Generated endpoints (unless disabled via `disable`):
|
|
156
|
+
POST / -> create_view (201, schema_out)
|
|
157
|
+
GET / -> list_view (200, List[schema_out] paginated)
|
|
158
|
+
GET /{pk} -> retrieve_view (200, schema_out)
|
|
159
|
+
PATCH /{pk}/ -> update_view (200, schema_out)
|
|
160
|
+
DELETE /{pk}/ -> delete_view (204)
|
|
161
|
+
|
|
162
|
+
M2M endpoints (per entry in m2m_relations) if enabled:
|
|
163
|
+
GET /{pk}/{related_path} -> list related objects (paginated)
|
|
164
|
+
POST /{pk}/{related_path}/ -> add/remove related objects (depending on m2m_add / m2m_remove)
|
|
165
|
+
|
|
166
|
+
M2M filters:
|
|
167
|
+
Each M2MRelationSchema may define a filters dict:
|
|
168
|
+
filters = { "field_name": (type, default) }
|
|
169
|
+
A dynamic Pydantic Filters schema is generated and exposed as query params
|
|
170
|
+
on the related GET endpoint: /{pk}/{related_path}?field_name=value.
|
|
171
|
+
To apply custom filter logic implement an hook named:
|
|
172
|
+
<related_name>_query_params_handler(self, queryset, filters_dict)
|
|
173
|
+
It receives the initial related queryset and the validated/dumped filters
|
|
174
|
+
dict, and must return the (optionally) filtered queryset.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
@api.viewset(model=models.User)
|
|
178
|
+
class UserViewSet(APIViewSet):
|
|
179
|
+
m2m_relations = [
|
|
180
|
+
M2MRelationSchema(
|
|
181
|
+
model=models.Tag,
|
|
182
|
+
related_name="tags",
|
|
183
|
+
filters={
|
|
184
|
+
"name": (str, "")
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def tags_query_params_handler(self, queryset, filters):
|
|
190
|
+
name_filter = filters.get("name")
|
|
191
|
+
if name_filter:
|
|
192
|
+
queryset = queryset.filter(name__icontains=name_filter)
|
|
193
|
+
return queryset
|
|
194
|
+
|
|
195
|
+
If filters is empty or omitted no query params are added for that relation.
|
|
196
|
+
|
|
197
|
+
Attribute summary:
|
|
198
|
+
model: Django model or ModelSerializer.
|
|
199
|
+
api: NinjaAPI instance.
|
|
200
|
+
schema_in / schema_out / schema_update: Pydantic schemas (auto when ModelSerializer).
|
|
201
|
+
auth: Default auth list or NOT_SET (no auth). Verb specific auth: get_auth, post_auth, patch_auth, delete_auth.
|
|
202
|
+
pagination_class: AsyncPaginationBase subclass (default PageNumberPagination).
|
|
203
|
+
query_params: Dict[str, (type, default)] to build a dynamic filters schema for list_view.
|
|
204
|
+
disable: List of view type strings: 'create','list','retrieve','update','delete','all'.
|
|
205
|
+
api_route_path: Base path; auto-resolved from verbose name if empty.
|
|
206
|
+
list_docs / create_docs / retrieve_docs / update_docs / delete_docs: Endpoint descriptions.
|
|
207
|
+
m2m_relations: List of M2MRelationSchema describing related model, related_name, custom path, auth, filters.
|
|
208
|
+
m2m_add / m2m_remove / m2m_get: Enable add/remove/get M2M operations.
|
|
209
|
+
m2m_auth: Auth list for all M2M endpoints unless overridden per relation.
|
|
210
|
+
|
|
211
|
+
Overridable hooks:
|
|
212
|
+
views(): Register extra custom endpoints on self.router.
|
|
213
|
+
query_params_handler(queryset, filters): Sync/Async hook to apply list filters.
|
|
214
|
+
<related_name>_query_params_handler(queryset, filters): Async hook for per-M2M filtering.
|
|
215
|
+
|
|
216
|
+
Error responses:
|
|
217
|
+
All endpoints may return GenericMessageSchema for codes in ERROR_CODES (400,401,404).
|
|
218
|
+
|
|
219
|
+
Internal:
|
|
220
|
+
Dynamic path/filter schemas built with pydantic.create_model.
|
|
221
|
+
unique_view decorator prevents duplicate registration.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
model: ModelSerializer | Model
|
|
225
|
+
serializer_class: serializers.Serializer | None = None
|
|
226
|
+
schema_in: Schema | None = None
|
|
227
|
+
schema_out: Schema | None = None
|
|
228
|
+
schema_update: Schema | None = None
|
|
229
|
+
get_auth: list | None = NOT_SET
|
|
230
|
+
post_auth: list | None = NOT_SET
|
|
231
|
+
patch_auth: list | None = NOT_SET
|
|
232
|
+
delete_auth: list | None = NOT_SET
|
|
233
|
+
pagination_class: type[AsyncPaginationBase] = PageNumberPagination
|
|
234
|
+
query_params: dict[str, tuple[type, ...]] = {}
|
|
235
|
+
disable: list[type[VIEW_TYPES]] = []
|
|
236
|
+
list_docs = "List all objects."
|
|
237
|
+
create_docs = "Create a new object."
|
|
238
|
+
retrieve_docs = "Retrieve a specific object by its primary key."
|
|
239
|
+
update_docs = "Update an object by its primary key."
|
|
240
|
+
delete_docs = "Delete an object by its primary key."
|
|
241
|
+
m2m_relations: list[M2MRelationSchema] = []
|
|
242
|
+
m2m_auth: list | None = NOT_SET
|
|
243
|
+
extra_decorators: DecoratorsSchema = DecoratorsSchema()
|
|
244
|
+
|
|
245
|
+
def __init__(
|
|
246
|
+
self,
|
|
247
|
+
api: NinjaAPI = None,
|
|
248
|
+
model: Model | ModelSerializer = None,
|
|
249
|
+
prefix: str = None,
|
|
250
|
+
tags: list[str] = None,
|
|
251
|
+
) -> None:
|
|
252
|
+
self.api = api or self.api
|
|
253
|
+
self.error_codes = ERROR_CODES
|
|
254
|
+
self.model = model or self.model
|
|
255
|
+
self.model_util = (
|
|
256
|
+
ModelUtil(self.model, serializer_class=self.serializer_class)
|
|
257
|
+
if not isinstance(self.model, ModelSerializerMeta)
|
|
258
|
+
else self.model.util
|
|
259
|
+
)
|
|
260
|
+
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
261
|
+
self.path_schema = self._generate_path_schema()
|
|
262
|
+
self.filters_schema = self._generate_filters_schema()
|
|
263
|
+
self.model_verbose_name = self.model._meta.verbose_name.capitalize()
|
|
264
|
+
self.router_tag = self.router_tag or self.model_verbose_name
|
|
265
|
+
self.router_tags = self.router_tags or tags or [self.router_tag]
|
|
266
|
+
self.router = Router(tags=self.router_tags)
|
|
267
|
+
self.append_slash = getattr(settings, "NINJA_AIO_APPEND_SLASH", True)
|
|
268
|
+
self.path = "/" if self.append_slash else ""
|
|
269
|
+
self.get_path = ""
|
|
270
|
+
self.get_path_retrieve = f"{{{self.model_util.model_pk_name}}}"
|
|
271
|
+
self.path_retrieve = (
|
|
272
|
+
f"{self.get_path_retrieve}/"
|
|
273
|
+
if self.append_slash
|
|
274
|
+
else self.get_path_retrieve
|
|
275
|
+
)
|
|
276
|
+
self.api_route_path = (
|
|
277
|
+
self.api_route_path
|
|
278
|
+
or prefix
|
|
279
|
+
or self.model_util.verbose_name_path_resolver()
|
|
280
|
+
)
|
|
281
|
+
self.m2m_api = (
|
|
282
|
+
None
|
|
283
|
+
if not self.m2m_relations
|
|
284
|
+
else ManyToManyAPI(relations=self.m2m_relations, view_set=self)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def _crud_views(self):
|
|
289
|
+
"""
|
|
290
|
+
Mapping of CRUD operation name to (response schema, view factory).
|
|
291
|
+
"""
|
|
292
|
+
return {
|
|
293
|
+
"create": (self.schema_in, self.create_view),
|
|
294
|
+
"list": (self.schema_out, self.list_view),
|
|
295
|
+
"retrieve": (self.schema_out, self.retrieve_view),
|
|
296
|
+
"update": (self.schema_update, self.update_view),
|
|
297
|
+
"delete": (None, self.delete_view),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
def _auth_view(self, view_type: str):
|
|
301
|
+
"""
|
|
302
|
+
Resolve auth for a specific HTTP verb; falls back to self.auth if NOT_SET.
|
|
303
|
+
"""
|
|
304
|
+
auth = getattr(self, f"{view_type}_auth", None)
|
|
305
|
+
return auth if auth is not NOT_SET else self.auth
|
|
306
|
+
|
|
307
|
+
def get_view_auth(self):
|
|
308
|
+
return self._auth_view("get")
|
|
309
|
+
|
|
310
|
+
def post_view_auth(self):
|
|
311
|
+
return self._auth_view("post")
|
|
312
|
+
|
|
313
|
+
def patch_view_auth(self):
|
|
314
|
+
return self._auth_view("patch")
|
|
315
|
+
|
|
316
|
+
def delete_view_auth(self):
|
|
317
|
+
return self._auth_view("delete")
|
|
318
|
+
|
|
319
|
+
def _generate_schema(self, fields: dict, name: str) -> Schema:
|
|
320
|
+
"""
|
|
321
|
+
Dynamically build a Pydantic model for path / filter schemas.
|
|
322
|
+
"""
|
|
323
|
+
return create_model(f"{self.model_util.model_name}{name}", **fields)
|
|
324
|
+
|
|
325
|
+
def _generate_path_schema(self):
|
|
326
|
+
"""
|
|
327
|
+
Schema containing only the primary key field for path resolution.
|
|
328
|
+
"""
|
|
329
|
+
return self._generate_schema(
|
|
330
|
+
{self.model_util.model_pk_name: self.model_util.pk_field_type}, "PathSchema"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def _generate_filters_schema(self):
|
|
334
|
+
"""
|
|
335
|
+
Build filters schema from query_params definition.
|
|
336
|
+
"""
|
|
337
|
+
return self._generate_schema(self.query_params, "FiltersSchema")
|
|
338
|
+
|
|
339
|
+
def _get_pk(self, data: Schema):
|
|
340
|
+
"""
|
|
341
|
+
Extract pk from a path schema instance.
|
|
342
|
+
"""
|
|
343
|
+
return data.model_dump()[self.model_util.model_pk_name]
|
|
344
|
+
|
|
345
|
+
def _get_query_data(self) -> ModelQuerySetSchema:
|
|
346
|
+
"""
|
|
347
|
+
Return default query data for list/retrieve views.
|
|
348
|
+
"""
|
|
349
|
+
return (
|
|
350
|
+
ModelQuerySetSchema()
|
|
351
|
+
if not isinstance(self.model, ModelSerializerMeta)
|
|
352
|
+
else self.model.query_util.read_config
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def get_schemas(self):
|
|
356
|
+
"""
|
|
357
|
+
Compute and return (schema_out, schema_in, schema_update).
|
|
358
|
+
|
|
359
|
+
- If model is a ModelSerializer (ModelSerializerMeta), auto-generate read/create/update schemas.
|
|
360
|
+
- Otherwise, use existing schemas or generate from serializer_class if provided.
|
|
361
|
+
"""
|
|
362
|
+
# ModelSerializer case: prefer explicitly set schemas, otherwise generate from the model
|
|
363
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
364
|
+
return (
|
|
365
|
+
self.schema_out or self.model.generate_read_s(),
|
|
366
|
+
self.schema_in or self.model.generate_create_s(),
|
|
367
|
+
self.schema_update or self.model.generate_update_s(),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Non-ModelSerializer: start from provided schemas
|
|
371
|
+
schema_out, schema_in, schema_update = (
|
|
372
|
+
self.schema_out,
|
|
373
|
+
self.schema_in,
|
|
374
|
+
self.schema_update,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# If a serializer_class is available, generate from it
|
|
378
|
+
if self.serializer_class:
|
|
379
|
+
schema_in = schema_in or self.serializer_class.generate_create_s()
|
|
380
|
+
schema_out = schema_out or self.serializer_class.generate_read_s()
|
|
381
|
+
schema_update = schema_update or self.serializer_class.generate_update_s()
|
|
382
|
+
|
|
383
|
+
return (schema_out, schema_in, schema_update)
|
|
384
|
+
|
|
385
|
+
async def query_params_handler(
|
|
386
|
+
self, queryset: QuerySet[ModelSerializer], filters: dict
|
|
387
|
+
):
|
|
388
|
+
"""
|
|
389
|
+
Override to apply custom filtering logic for list_view.
|
|
390
|
+
filters is already validated and dumped.
|
|
391
|
+
Return the (possibly modified) queryset.
|
|
392
|
+
"""
|
|
393
|
+
return queryset
|
|
394
|
+
|
|
395
|
+
def create_view(self):
|
|
396
|
+
"""
|
|
397
|
+
Register create endpoint.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
@self.router.post(
|
|
401
|
+
self.path,
|
|
402
|
+
auth=self.post_view_auth(),
|
|
403
|
+
summary=f"Create {self.model._meta.verbose_name.capitalize()}",
|
|
404
|
+
description=self.create_docs,
|
|
405
|
+
response={201: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
406
|
+
)
|
|
407
|
+
@decorate_view(unique_view(self), *self.extra_decorators.create)
|
|
408
|
+
async def create(request: HttpRequest, data: self.schema_in): # type: ignore
|
|
409
|
+
return 201, await self.model_util.create_s(request, data, self.schema_out)
|
|
410
|
+
|
|
411
|
+
return create
|
|
412
|
+
|
|
413
|
+
def list_view(self):
|
|
414
|
+
"""
|
|
415
|
+
Register list endpoint with pagination and optional filters.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
@self.router.get(
|
|
419
|
+
self.get_path,
|
|
420
|
+
auth=self.get_view_auth(),
|
|
421
|
+
summary=f"List {self.model._meta.verbose_name_plural.capitalize()}",
|
|
422
|
+
description=self.list_docs,
|
|
423
|
+
response={
|
|
424
|
+
200: List[self.schema_out],
|
|
425
|
+
self.error_codes: GenericMessageSchema,
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
@decorate_view(
|
|
429
|
+
paginate(self.pagination_class),
|
|
430
|
+
unique_view(self, plural=True),
|
|
431
|
+
*self.extra_decorators.list,
|
|
432
|
+
)
|
|
433
|
+
async def list(
|
|
434
|
+
request: HttpRequest,
|
|
435
|
+
filters: Query[self.filters_schema] = None, # type: ignore
|
|
436
|
+
):
|
|
437
|
+
qs = await self.model_util.get_objects(
|
|
438
|
+
request,
|
|
439
|
+
query_data=self._get_query_data(),
|
|
440
|
+
is_for_read=True,
|
|
441
|
+
)
|
|
442
|
+
if filters is not None:
|
|
443
|
+
qs = await self.query_params_handler(qs, filters.model_dump())
|
|
444
|
+
return await self.model_util.list_read_s(self.schema_out, request, qs)
|
|
445
|
+
|
|
446
|
+
return list
|
|
447
|
+
|
|
448
|
+
def retrieve_view(self):
|
|
449
|
+
"""
|
|
450
|
+
Register retrieve endpoint.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
@self.router.get(
|
|
454
|
+
self.get_path_retrieve,
|
|
455
|
+
auth=self.get_view_auth(),
|
|
456
|
+
summary=f"Retrieve {self.model._meta.verbose_name.capitalize()}",
|
|
457
|
+
description=self.retrieve_docs,
|
|
458
|
+
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
459
|
+
)
|
|
460
|
+
@decorate_view(unique_view(self), *self.extra_decorators.retrieve)
|
|
461
|
+
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
462
|
+
query_data = self._get_query_data()
|
|
463
|
+
return await self.model_util.read_s(
|
|
464
|
+
self.schema_out,
|
|
465
|
+
request,
|
|
466
|
+
query_data=QuerySchema(
|
|
467
|
+
getters={"pk": self._get_pk(pk)}, **query_data.model_dump()
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
return retrieve
|
|
472
|
+
|
|
473
|
+
def update_view(self):
|
|
474
|
+
"""
|
|
475
|
+
Register update endpoint.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
@self.router.patch(
|
|
479
|
+
self.path_retrieve,
|
|
480
|
+
auth=self.patch_view_auth(),
|
|
481
|
+
summary=f"Update {self.model._meta.verbose_name.capitalize()}",
|
|
482
|
+
description=self.update_docs,
|
|
483
|
+
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
484
|
+
)
|
|
485
|
+
@decorate_view(unique_view(self), *self.extra_decorators.update)
|
|
486
|
+
async def update(
|
|
487
|
+
request: HttpRequest,
|
|
488
|
+
data: self.schema_update, # type: ignore
|
|
489
|
+
pk: Path[self.path_schema], # type: ignore
|
|
490
|
+
):
|
|
491
|
+
return await self.model_util.update_s(
|
|
492
|
+
request, data, self._get_pk(pk), self.schema_out
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return update
|
|
496
|
+
|
|
497
|
+
def delete_view(self):
|
|
498
|
+
"""
|
|
499
|
+
Register delete endpoint.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
@self.router.delete(
|
|
503
|
+
self.path_retrieve,
|
|
504
|
+
auth=self.delete_view_auth(),
|
|
505
|
+
summary=f"Delete {self.model._meta.verbose_name.capitalize()}",
|
|
506
|
+
description=self.delete_docs,
|
|
507
|
+
response={204: None, self.error_codes: GenericMessageSchema},
|
|
508
|
+
)
|
|
509
|
+
@decorate_view(unique_view(self), *self.extra_decorators.delete)
|
|
510
|
+
async def delete(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
511
|
+
return 204, await self.model_util.delete_s(request, self._get_pk(pk))
|
|
512
|
+
|
|
513
|
+
return delete
|
|
514
|
+
|
|
515
|
+
def views(self):
|
|
516
|
+
"""
|
|
517
|
+
Override to register custom non-CRUD endpoints on self.router.
|
|
518
|
+
Use auth=self.auth or verb specific auth attributes if needed.
|
|
519
|
+
"""
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
def _set_additional_views(self):
|
|
523
|
+
self.views()
|
|
524
|
+
if self.m2m_api is not None:
|
|
525
|
+
self.m2m_api._add_views()
|
|
526
|
+
return self.router
|
|
527
|
+
|
|
528
|
+
def _add_views(self):
|
|
529
|
+
"""
|
|
530
|
+
Register CRUD (unless disabled), custom views, and M2M endpoints.
|
|
531
|
+
If 'all' in disable only CRUD is skipped; M2M + custom still added.
|
|
532
|
+
"""
|
|
533
|
+
super()._add_views()
|
|
534
|
+
if "all" in self.disable:
|
|
535
|
+
return self._set_additional_views()
|
|
536
|
+
|
|
537
|
+
for views_type, (schema, view) in self._crud_views.items():
|
|
538
|
+
if views_type not in self.disable and (
|
|
539
|
+
schema is not None or views_type == "delete"
|
|
540
|
+
):
|
|
541
|
+
view()
|
|
542
|
+
|
|
543
|
+
return self._set_additional_views()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class ReadOnlyViewSet(APIViewSet):
|
|
547
|
+
"""
|
|
548
|
+
ReadOnly viewset generating async List + Retrieve endpoints for a Django model.
|
|
549
|
+
|
|
550
|
+
Usage:
|
|
551
|
+
@api.viewset(model=MyModel)
|
|
552
|
+
class MyModelReadOnlyViewSet(ReadOnlyViewSet):
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
or
|
|
556
|
+
|
|
557
|
+
class MyModelReadOnlyViewSet(ReadOnlyViewSet):
|
|
558
|
+
model = MyModel
|
|
559
|
+
api = api
|
|
560
|
+
MyModelReadOnlyViewSet().add_views_to_route()
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
disable = ["create", "update", "delete"]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class WriteOnlyViewSet(APIViewSet):
|
|
567
|
+
"""
|
|
568
|
+
WriteOnly viewset generating async Create + Update + Delete endpoints for a Django model.
|
|
569
|
+
|
|
570
|
+
Usage:
|
|
571
|
+
@api.viewset(model=MyModel)
|
|
572
|
+
class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
or
|
|
576
|
+
|
|
577
|
+
class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
|
|
578
|
+
model = MyModel
|
|
579
|
+
api = api
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
disable = ["list", "retrieve"]
|