django-ninja-aio-crud 2.8.0__py3-none-any.whl → 2.10.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.
Potentially problematic release.
This version of django-ninja-aio-crud might be problematic. Click here for more details.
- {django_ninja_aio_crud-2.8.0.dist-info → django_ninja_aio_crud-2.10.0.dist-info}/METADATA +1 -1
- {django_ninja_aio_crud-2.8.0.dist-info → django_ninja_aio_crud-2.10.0.dist-info}/RECORD +13 -13
- ninja_aio/__init__.py +1 -1
- ninja_aio/helpers/query.py +3 -0
- ninja_aio/models/serializers.py +19 -9
- ninja_aio/models/utils.py +117 -66
- ninja_aio/schemas/__init__.py +2 -0
- ninja_aio/schemas/api.py +20 -1
- ninja_aio/schemas/helpers.py +2 -0
- ninja_aio/views/api.py +5 -2
- ninja_aio/views/mixins.py +63 -0
- {django_ninja_aio_crud-2.8.0.dist-info → django_ninja_aio_crud-2.10.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.8.0.dist-info → django_ninja_aio_crud-2.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=FZKyvJwguGusKjYpVUXSYB45Fssox9yEFHg47WX0BaE,120
|
|
2
2
|
ninja_aio/api.py,sha256=tuC7vdvn7s1GkCnSFy9Kn1zv0glZfYptRQVvo8ZRtGQ,2429
|
|
3
3
|
ninja_aio/auth.py,sha256=4sWdFPjKiQgUL1d_CSGDblVjnY5ptP6LQha6XXdluJA,9157
|
|
4
4
|
ninja_aio/exceptions.py,sha256=_3xFqfFCOfrrMhSA0xbMqgXy8R0UQjhXaExrFvaDAjY,3891
|
|
@@ -12,18 +12,18 @@ ninja_aio/factory/__init__.py,sha256=IdH2z1ZZpv_IqonaDfVo7IsMzkgop6lHqz42RphUYBU
|
|
|
12
12
|
ninja_aio/factory/operations.py,sha256=OgWGqq4WJ4arSQrH9FGAby9kx-HTdS7MOITxHdYMk18,12051
|
|
13
13
|
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
ninja_aio/helpers/api.py,sha256=2beyexep-ehgaA_1bV5Yuh3zRDVcRCMkrW94nmfDWEA,20819
|
|
15
|
-
ninja_aio/helpers/query.py,sha256=
|
|
15
|
+
ninja_aio/helpers/query.py,sha256=NMGkS_v-ZVYKNtf1XohEUzfwca52Eq5FTcQ5lehHjus,4682
|
|
16
16
|
ninja_aio/models/__init__.py,sha256=L3UQnQAlKoI3F7jinadL-Nn55hkPvnSRPYW0JtnbWFo,114
|
|
17
|
-
ninja_aio/models/serializers.py,sha256=
|
|
18
|
-
ninja_aio/models/utils.py,sha256=
|
|
19
|
-
ninja_aio/schemas/__init__.py,sha256=
|
|
20
|
-
ninja_aio/schemas/api.py,sha256
|
|
17
|
+
ninja_aio/models/serializers.py,sha256=I7pUz_vl0FElaVsrGaogT_Lj9T4uaNG7UDMGf5VMwW4,38468
|
|
18
|
+
ninja_aio/models/utils.py,sha256=dxzwqE25b0lFZ--30XxeIJB-y4xbIDtc624vGaa_wOg,32885
|
|
19
|
+
ninja_aio/schemas/__init__.py,sha256=_a092xZezlLc9QWCPWrybeklByCs39jbvf33zwdnrys,603
|
|
20
|
+
ninja_aio/schemas/api.py,sha256=InzZgIFU4Zxkgj9u_zZzAMYs_vdaPD5eu12gG7xAFoQ,1047
|
|
21
21
|
ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
|
|
22
|
-
ninja_aio/schemas/helpers.py,sha256=
|
|
22
|
+
ninja_aio/schemas/helpers.py,sha256=Vti5BfHWpxaJXj_ixZBJb34VRwhHODrlVjRlIuHh_ug,8428
|
|
23
23
|
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
24
|
-
ninja_aio/views/api.py,sha256=
|
|
25
|
-
ninja_aio/views/mixins.py,sha256=
|
|
26
|
-
django_ninja_aio_crud-2.
|
|
27
|
-
django_ninja_aio_crud-2.
|
|
28
|
-
django_ninja_aio_crud-2.
|
|
29
|
-
django_ninja_aio_crud-2.
|
|
24
|
+
ninja_aio/views/api.py,sha256=CW74JB1tsdWfv3Y3x4K9o8yXsrMxCKWZ4QalhqJGan8,22072
|
|
25
|
+
ninja_aio/views/mixins.py,sha256=01kEGMmpD3eTd8R5mX8sNr7KoizOfN0wvbV5y180IdE,13835
|
|
26
|
+
django_ninja_aio_crud-2.10.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
27
|
+
django_ninja_aio_crud-2.10.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
28
|
+
django_ninja_aio_crud-2.10.0.dist-info/METADATA,sha256=-cSLMUj-s7djNL2RxRhLNkErv0yM49NK8CA8-zp-4zw,9964
|
|
29
|
+
django_ninja_aio_crud-2.10.0.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
ninja_aio/helpers/query.py
CHANGED
|
@@ -75,6 +75,9 @@ class QueryUtil:
|
|
|
75
75
|
self.queryset_request_config: ModelQuerySetSchema = self._configs.get(
|
|
76
76
|
self.SCOPES.QUERYSET_REQUEST, ModelQuerySetSchema()
|
|
77
77
|
)
|
|
78
|
+
self.detail_config: ModelQuerySetSchema = self._configs.get(
|
|
79
|
+
self.SCOPES.DETAIL, ModelQuerySetSchema()
|
|
80
|
+
)
|
|
78
81
|
|
|
79
82
|
def _get_config(self, conf_name: str) -> ModelQuerySetSchema:
|
|
80
83
|
"""Helper method to retrieve configuration attributes."""
|
ninja_aio/models/serializers.py
CHANGED
|
@@ -61,6 +61,7 @@ class BaseSerializer:
|
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
read = ModelQuerySetSchema()
|
|
64
|
+
detail = ModelQuerySetSchema()
|
|
64
65
|
queryset_request = ModelQuerySetSchema()
|
|
65
66
|
extras: list[ModelQuerySetExtraSchema] = []
|
|
66
67
|
|
|
@@ -325,14 +326,18 @@ class BaseSerializer:
|
|
|
325
326
|
]
|
|
326
327
|
|
|
327
328
|
@classmethod
|
|
328
|
-
def get_excluded_fields(cls, s_type:
|
|
329
|
+
def get_excluded_fields(cls, s_type: S_TYPES):
|
|
329
330
|
"""Return excluded field names for the serializer type."""
|
|
330
331
|
return cls._get_fields(s_type, "excludes")
|
|
331
332
|
|
|
332
333
|
@classmethod
|
|
333
|
-
def get_fields(cls, s_type:
|
|
334
|
+
def get_fields(cls, s_type: S_TYPES):
|
|
334
335
|
"""Return explicit declared fields for the serializer type."""
|
|
335
|
-
|
|
336
|
+
fields = cls._get_fields(s_type, "fields")
|
|
337
|
+
# Detail schema falls back to read fields if none declared
|
|
338
|
+
if not fields and s_type == "detail":
|
|
339
|
+
return cls._get_fields("read", "fields")
|
|
340
|
+
return fields
|
|
336
341
|
|
|
337
342
|
@classmethod
|
|
338
343
|
def is_custom(cls, field: str) -> bool:
|
|
@@ -406,7 +411,11 @@ class BaseSerializer:
|
|
|
406
411
|
"""Check if field is a reverse relation (M2M, reverse FK, reverse O2O)."""
|
|
407
412
|
return isinstance(
|
|
408
413
|
field_obj,
|
|
409
|
-
(
|
|
414
|
+
(
|
|
415
|
+
ManyToManyDescriptor,
|
|
416
|
+
ReverseManyToOneDescriptor,
|
|
417
|
+
ReverseOneToOneDescriptor,
|
|
418
|
+
),
|
|
410
419
|
)
|
|
411
420
|
|
|
412
421
|
@classmethod
|
|
@@ -419,9 +428,8 @@ class BaseSerializer:
|
|
|
419
428
|
@classmethod
|
|
420
429
|
def _warn_missing_relation_serializer(cls, field_name: str, model) -> None:
|
|
421
430
|
"""Emit warning for reverse relations without explicit serializer mapping."""
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
and not getattr(settings, "NINJA_AIO_TESTING", False)
|
|
431
|
+
if not isinstance(model, ModelSerializerMeta) and not getattr(
|
|
432
|
+
settings, "NINJA_AIO_TESTING", False
|
|
425
433
|
):
|
|
426
434
|
warnings.warn(
|
|
427
435
|
f"{cls.__name__}: reverse relation '{field_name}' is listed in read fields "
|
|
@@ -469,7 +477,9 @@ class BaseSerializer:
|
|
|
469
477
|
tuple: (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
|
|
470
478
|
"""
|
|
471
479
|
if schema_type not in ("Out", "Detail"):
|
|
472
|
-
raise ValueError(
|
|
480
|
+
raise ValueError(
|
|
481
|
+
"get_schema_out_data only supports 'Out' or 'Detail' types"
|
|
482
|
+
)
|
|
473
483
|
|
|
474
484
|
fields_type = "read" if schema_type == "Out" else "detail"
|
|
475
485
|
model = cls._get_model()
|
|
@@ -602,7 +612,7 @@ class BaseSerializer:
|
|
|
602
612
|
@classmethod
|
|
603
613
|
def generate_detail_s(cls, depth: int = 1) -> Schema:
|
|
604
614
|
"""Generate detail (single object Out) schema."""
|
|
605
|
-
return cls._generate_model_schema("Detail", depth)
|
|
615
|
+
return cls._generate_model_schema("Detail", depth) or cls.generate_read_s(depth)
|
|
606
616
|
|
|
607
617
|
@classmethod
|
|
608
618
|
def generate_create_s(cls) -> Schema:
|
ninja_aio/models/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Literal
|
|
4
4
|
|
|
5
5
|
from ninja import Schema
|
|
6
6
|
from ninja.orm import fields
|
|
@@ -167,9 +167,19 @@ class ModelUtil:
|
|
|
167
167
|
list[str]
|
|
168
168
|
Explicit read fields if ModelSerializerMeta, otherwise all model fields.
|
|
169
169
|
"""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
return self._get_serializable_field_names("read")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def serializable_detail_fields(self):
|
|
174
|
+
"""
|
|
175
|
+
List of fields considered serializable for detail operations.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
list[str]
|
|
180
|
+
Explicit detail fields if ModelSerializerMeta, otherwise all model fields.
|
|
181
|
+
"""
|
|
182
|
+
return self._get_serializable_field_names("detail")
|
|
173
183
|
|
|
174
184
|
@property
|
|
175
185
|
def model_fields(self):
|
|
@@ -235,12 +245,29 @@ class ModelUtil:
|
|
|
235
245
|
"""
|
|
236
246
|
return self.model_verbose_name_plural.replace(" ", "")
|
|
237
247
|
|
|
248
|
+
def _get_serializable_field_names(
|
|
249
|
+
self, fields_type: Literal["read", "detail"]
|
|
250
|
+
) -> list[str]:
|
|
251
|
+
"""
|
|
252
|
+
Get serializable field names for the model.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list[str]
|
|
257
|
+
List of serializable field names.
|
|
258
|
+
"""
|
|
259
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
260
|
+
return self.model.get_fields(fields_type)
|
|
261
|
+
if self.with_serializer:
|
|
262
|
+
return self.serializer_class.get_fields(fields_type)
|
|
263
|
+
return self.model_fields
|
|
264
|
+
|
|
238
265
|
async def _get_base_queryset(
|
|
239
266
|
self,
|
|
240
267
|
request: HttpRequest,
|
|
241
268
|
query_data: QuerySchema,
|
|
242
269
|
with_qs_request: bool,
|
|
243
|
-
|
|
270
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
244
271
|
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
245
272
|
"""
|
|
246
273
|
Build base queryset with optimizations and filters.
|
|
@@ -253,8 +280,9 @@ class ModelUtil:
|
|
|
253
280
|
Query configuration with filters and optimizations.
|
|
254
281
|
with_qs_request : bool
|
|
255
282
|
Whether to apply queryset_request hook.
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
is_for : Literal["read", "detail"] | None
|
|
284
|
+
Purpose of the query, determines which serializable fields to use.
|
|
285
|
+
If None, only query_data optimizations are applied.
|
|
258
286
|
|
|
259
287
|
Returns
|
|
260
288
|
-------
|
|
@@ -269,7 +297,7 @@ class ModelUtil:
|
|
|
269
297
|
)
|
|
270
298
|
|
|
271
299
|
# Apply query optimizations
|
|
272
|
-
obj_qs = self._apply_query_optimizations(obj_qs, query_data,
|
|
300
|
+
obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for)
|
|
273
301
|
|
|
274
302
|
# Apply queryset_request hook if available
|
|
275
303
|
if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
|
|
@@ -286,7 +314,7 @@ class ModelUtil:
|
|
|
286
314
|
request: HttpRequest,
|
|
287
315
|
query_data: ObjectsQuerySchema = None,
|
|
288
316
|
with_qs_request=True,
|
|
289
|
-
|
|
317
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
290
318
|
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
291
319
|
"""
|
|
292
320
|
Retrieve a queryset with optimized database queries.
|
|
@@ -305,9 +333,9 @@ class ModelUtil:
|
|
|
305
333
|
with_qs_request : bool, optional
|
|
306
334
|
Whether to apply the model's queryset_request hook if available.
|
|
307
335
|
Defaults to True.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
336
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
337
|
+
Purpose of the query, determines which serializable fields to use.
|
|
338
|
+
If None, only query_data optimizations are applied.
|
|
311
339
|
|
|
312
340
|
Returns
|
|
313
341
|
-------
|
|
@@ -323,7 +351,7 @@ class ModelUtil:
|
|
|
323
351
|
query_data = ObjectsQuerySchema()
|
|
324
352
|
|
|
325
353
|
return await self._get_base_queryset(
|
|
326
|
-
request, query_data, with_qs_request,
|
|
354
|
+
request, query_data, with_qs_request, is_for
|
|
327
355
|
)
|
|
328
356
|
|
|
329
357
|
async def get_object(
|
|
@@ -332,7 +360,7 @@ class ModelUtil:
|
|
|
332
360
|
pk: int | str = None,
|
|
333
361
|
query_data: ObjectQuerySchema = None,
|
|
334
362
|
with_qs_request=True,
|
|
335
|
-
|
|
363
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
336
364
|
) -> type["ModelSerializer"] | models.Model:
|
|
337
365
|
"""
|
|
338
366
|
Retrieve a single object with optimized database queries.
|
|
@@ -353,9 +381,9 @@ class ModelUtil:
|
|
|
353
381
|
with_qs_request : bool, optional
|
|
354
382
|
Whether to apply the model's queryset_request hook if available.
|
|
355
383
|
Defaults to True.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
384
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
385
|
+
Purpose of the query, determines which serializable fields to use.
|
|
386
|
+
If None, only query_data optimizations are applied.
|
|
359
387
|
|
|
360
388
|
Returns
|
|
361
389
|
-------
|
|
@@ -385,7 +413,7 @@ class ModelUtil:
|
|
|
385
413
|
# Build lookup query and get optimized queryset
|
|
386
414
|
get_q = self._build_lookup_query(pk, query_data.getters)
|
|
387
415
|
obj_qs = await self._get_base_queryset(
|
|
388
|
-
request, query_data, with_qs_request,
|
|
416
|
+
request, query_data, with_qs_request, is_for
|
|
389
417
|
)
|
|
390
418
|
|
|
391
419
|
# Perform lookup
|
|
@@ -421,7 +449,7 @@ class ModelUtil:
|
|
|
421
449
|
self,
|
|
422
450
|
queryset: models.QuerySet,
|
|
423
451
|
query_data: QuerySchema,
|
|
424
|
-
|
|
452
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
425
453
|
) -> models.QuerySet:
|
|
426
454
|
"""
|
|
427
455
|
Apply select_related and prefetch_related optimizations to queryset.
|
|
@@ -432,8 +460,9 @@ class ModelUtil:
|
|
|
432
460
|
Base queryset to optimize.
|
|
433
461
|
query_data : ModelQuerySchema
|
|
434
462
|
Query configuration with select_related/prefetch_related lists.
|
|
435
|
-
|
|
436
|
-
|
|
463
|
+
is_for : Literal["read", "detail"] | None
|
|
464
|
+
Purpose of the query, determines which serializable fields to use.
|
|
465
|
+
If None, only query_data optimizations are applied.
|
|
437
466
|
|
|
438
467
|
Returns
|
|
439
468
|
-------
|
|
@@ -441,13 +470,13 @@ class ModelUtil:
|
|
|
441
470
|
Optimized queryset.
|
|
442
471
|
"""
|
|
443
472
|
select_related = (
|
|
444
|
-
query_data.select_related + self.get_select_relateds()
|
|
445
|
-
if
|
|
473
|
+
query_data.select_related + self.get_select_relateds(is_for)
|
|
474
|
+
if is_for
|
|
446
475
|
else query_data.select_related
|
|
447
476
|
)
|
|
448
477
|
prefetch_related = (
|
|
449
|
-
query_data.prefetch_related + self.get_reverse_relations()
|
|
450
|
-
if
|
|
478
|
+
query_data.prefetch_related + self.get_reverse_relations(is_for)
|
|
479
|
+
if is_for
|
|
451
480
|
else query_data.prefetch_related
|
|
452
481
|
)
|
|
453
482
|
|
|
@@ -458,36 +487,52 @@ class ModelUtil:
|
|
|
458
487
|
|
|
459
488
|
return queryset
|
|
460
489
|
|
|
461
|
-
def _get_read_optimizations(
|
|
490
|
+
def _get_read_optimizations(
|
|
491
|
+
self, is_for: Literal["read", "detail"] = "read"
|
|
492
|
+
) -> ModelQuerySetSchema:
|
|
462
493
|
"""
|
|
463
494
|
Retrieve read optimizations from model or serializer class.
|
|
464
495
|
|
|
496
|
+
When is_for="detail" and no detail config exists, falls back to read config.
|
|
497
|
+
|
|
465
498
|
Returns
|
|
466
499
|
-------
|
|
467
500
|
ModelQuerySetSchema
|
|
468
501
|
Read optimization configuration.
|
|
469
502
|
"""
|
|
470
503
|
if isinstance(self.model, ModelSerializerMeta):
|
|
471
|
-
|
|
504
|
+
result = getattr(self.model.QuerySet, is_for, None)
|
|
505
|
+
if result is None and is_for == "detail":
|
|
506
|
+
result = getattr(self.model.QuerySet, "read", None)
|
|
507
|
+
return result or ModelQuerySetSchema()
|
|
472
508
|
if self.with_serializer:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
509
|
+
result = getattr(self.serializer_class.QuerySet, is_for, None)
|
|
510
|
+
if result is None and is_for == "detail":
|
|
511
|
+
result = getattr(self.serializer_class.QuerySet, "read", None)
|
|
512
|
+
return result or ModelQuerySetSchema()
|
|
476
513
|
return ModelQuerySetSchema()
|
|
477
514
|
|
|
478
|
-
def get_reverse_relations(
|
|
515
|
+
def get_reverse_relations(
|
|
516
|
+
self, is_for: Literal["read", "detail"] = "read"
|
|
517
|
+
) -> list[str]:
|
|
479
518
|
"""
|
|
480
519
|
Discover reverse relation names for safe prefetching.
|
|
481
520
|
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
is_for : Literal["read", "detail"]
|
|
524
|
+
Purpose of the query, determines which serializable fields to use.
|
|
525
|
+
|
|
482
526
|
Returns
|
|
483
527
|
-------
|
|
484
528
|
list[str]
|
|
485
529
|
Relation attribute names.
|
|
486
530
|
"""
|
|
487
|
-
reverse_rels = self._get_read_optimizations().prefetch_related.copy()
|
|
531
|
+
reverse_rels = self._get_read_optimizations(is_for).prefetch_related.copy()
|
|
488
532
|
if reverse_rels:
|
|
489
533
|
return reverse_rels
|
|
490
|
-
|
|
534
|
+
serializable_fields = self._get_serializable_field_names(is_for)
|
|
535
|
+
for f in serializable_fields:
|
|
491
536
|
field_obj = getattr(self.model, f)
|
|
492
537
|
if isinstance(field_obj, ManyToManyDescriptor):
|
|
493
538
|
reverse_rels.append(f)
|
|
@@ -499,24 +544,32 @@ class ModelUtil:
|
|
|
499
544
|
reverse_rels.append(field_obj.related.name)
|
|
500
545
|
return reverse_rels
|
|
501
546
|
|
|
502
|
-
def get_select_relateds(
|
|
547
|
+
def get_select_relateds(
|
|
548
|
+
self, is_for: Literal["read", "detail"] = "read"
|
|
549
|
+
) -> list[str]:
|
|
503
550
|
"""
|
|
504
551
|
Discover forward relation names for safe select_related.
|
|
505
552
|
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
is_for : Literal["read", "detail"]
|
|
556
|
+
Purpose of the query, determines which serializable fields to use.
|
|
557
|
+
|
|
506
558
|
Returns
|
|
507
559
|
-------
|
|
508
560
|
list[str]
|
|
509
561
|
Relation attribute names.
|
|
510
562
|
"""
|
|
511
|
-
select_rels = self._get_read_optimizations().select_related.copy()
|
|
563
|
+
select_rels = self._get_read_optimizations(is_for).select_related.copy()
|
|
512
564
|
if select_rels:
|
|
513
565
|
return select_rels
|
|
514
|
-
|
|
566
|
+
serializable_fields = self._get_serializable_field_names(is_for)
|
|
567
|
+
for f in serializable_fields:
|
|
515
568
|
field_obj = getattr(self.model, f)
|
|
516
|
-
if isinstance(field_obj,
|
|
569
|
+
if isinstance(field_obj, ForwardOneToOneDescriptor):
|
|
517
570
|
select_rels.append(f)
|
|
518
571
|
continue
|
|
519
|
-
if isinstance(field_obj,
|
|
572
|
+
if isinstance(field_obj, ForwardManyToOneDescriptor):
|
|
520
573
|
select_rels.append(f)
|
|
521
574
|
return select_rels
|
|
522
575
|
|
|
@@ -577,17 +630,15 @@ class ModelUtil:
|
|
|
577
630
|
request: HttpRequest,
|
|
578
631
|
query_data: QuerySchema,
|
|
579
632
|
schema: Schema,
|
|
580
|
-
|
|
633
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
581
634
|
):
|
|
582
635
|
"""Handle different query modes (filters vs getters)."""
|
|
583
636
|
if hasattr(query_data, "filters") and query_data.filters:
|
|
584
|
-
return await self._serialize_queryset(
|
|
585
|
-
request, query_data, schema, is_for_read
|
|
586
|
-
)
|
|
637
|
+
return await self._serialize_queryset(request, query_data, schema, is_for)
|
|
587
638
|
|
|
588
639
|
if hasattr(query_data, "getters") and query_data.getters:
|
|
589
640
|
return await self._serialize_single_object(
|
|
590
|
-
request, query_data, schema,
|
|
641
|
+
request, query_data, schema, is_for
|
|
591
642
|
)
|
|
592
643
|
|
|
593
644
|
raise SerializeError(
|
|
@@ -599,12 +650,10 @@ class ModelUtil:
|
|
|
599
650
|
request: HttpRequest,
|
|
600
651
|
query_data: QuerySchema,
|
|
601
652
|
schema: Schema,
|
|
602
|
-
|
|
653
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
603
654
|
):
|
|
604
655
|
"""Serialize a queryset of objects."""
|
|
605
|
-
objs = await self.get_objects(
|
|
606
|
-
request, query_data=query_data, is_for_read=is_for_read
|
|
607
|
-
)
|
|
656
|
+
objs = await self.get_objects(request, query_data=query_data, is_for=is_for)
|
|
608
657
|
return [await self._bump_object_from_schema(obj, schema) async for obj in objs]
|
|
609
658
|
|
|
610
659
|
async def _serialize_single_object(
|
|
@@ -612,12 +661,10 @@ class ModelUtil:
|
|
|
612
661
|
request: HttpRequest,
|
|
613
662
|
query_data: QuerySchema,
|
|
614
663
|
obj_schema: Schema,
|
|
615
|
-
|
|
664
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
616
665
|
):
|
|
617
666
|
"""Serialize a single object."""
|
|
618
|
-
obj = await self.get_object(
|
|
619
|
-
request, query_data=query_data, is_for_read=is_for_read
|
|
620
|
-
)
|
|
667
|
+
obj = await self.get_object(request, query_data=query_data, is_for=is_for)
|
|
621
668
|
return await self._bump_object_from_schema(obj, obj_schema)
|
|
622
669
|
|
|
623
670
|
async def parse_input_data(self, request: HttpRequest, data: Schema):
|
|
@@ -649,7 +696,9 @@ class ModelUtil:
|
|
|
649
696
|
"""
|
|
650
697
|
payload = data.model_dump(mode="json")
|
|
651
698
|
|
|
652
|
-
is_serializer =
|
|
699
|
+
is_serializer = (
|
|
700
|
+
isinstance(self.model, ModelSerializerMeta) or self.with_serializer
|
|
701
|
+
)
|
|
653
702
|
serializer = self.serializer if self.with_serializer else self.model
|
|
654
703
|
|
|
655
704
|
# Collect custom and optional fields (only if ModelSerializerMeta)
|
|
@@ -732,7 +781,7 @@ class ModelUtil:
|
|
|
732
781
|
| type["ModelSerializer"]
|
|
733
782
|
| models.Model = None,
|
|
734
783
|
query_data: QuerySchema = None,
|
|
735
|
-
|
|
784
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
736
785
|
):
|
|
737
786
|
"""
|
|
738
787
|
Internal serialization method handling both single instances and querysets.
|
|
@@ -747,8 +796,8 @@ class ModelUtil:
|
|
|
747
796
|
Instance(s) to serialize. If None, fetches based on query_data.
|
|
748
797
|
query_data : QuerySchema, optional
|
|
749
798
|
Query parameters for fetching objects when instance is None.
|
|
750
|
-
|
|
751
|
-
|
|
799
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
800
|
+
Purpose of the query, determines which serializable fields to use.
|
|
752
801
|
|
|
753
802
|
Returns
|
|
754
803
|
-------
|
|
@@ -772,7 +821,7 @@ class ModelUtil:
|
|
|
772
821
|
return await self._bump_object_from_schema(instance, schema)
|
|
773
822
|
|
|
774
823
|
self._validate_read_params(request, query_data)
|
|
775
|
-
return await self._handle_query_mode(request, query_data, schema,
|
|
824
|
+
return await self._handle_query_mode(request, query_data, schema, is_for)
|
|
776
825
|
|
|
777
826
|
async def read_s(
|
|
778
827
|
self,
|
|
@@ -780,7 +829,7 @@ class ModelUtil:
|
|
|
780
829
|
request: HttpRequest = None,
|
|
781
830
|
instance: type["ModelSerializer"] = None,
|
|
782
831
|
query_data: ObjectQuerySchema = None,
|
|
783
|
-
|
|
832
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
784
833
|
) -> dict:
|
|
785
834
|
"""
|
|
786
835
|
Serialize a single model instance or fetch and serialize using query parameters.
|
|
@@ -799,8 +848,9 @@ class ModelUtil:
|
|
|
799
848
|
query_data : ObjectQuerySchema, optional
|
|
800
849
|
Query parameters with getters for single object lookup.
|
|
801
850
|
Required when instance is None.
|
|
802
|
-
|
|
803
|
-
|
|
851
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
852
|
+
Purpose of the query, determines which serializable fields to use.
|
|
853
|
+
Defaults to None.
|
|
804
854
|
|
|
805
855
|
Returns
|
|
806
856
|
-------
|
|
@@ -820,14 +870,14 @@ class ModelUtil:
|
|
|
820
870
|
-----
|
|
821
871
|
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
822
872
|
- When instance is provided, request and query_data are ignored
|
|
823
|
-
- Query optimizations applied when
|
|
873
|
+
- Query optimizations applied when is_for is specified
|
|
824
874
|
"""
|
|
825
875
|
return await self._read_s(
|
|
826
876
|
schema,
|
|
827
877
|
request,
|
|
828
878
|
instance,
|
|
829
879
|
query_data,
|
|
830
|
-
|
|
880
|
+
is_for,
|
|
831
881
|
)
|
|
832
882
|
|
|
833
883
|
async def list_read_s(
|
|
@@ -836,7 +886,7 @@ class ModelUtil:
|
|
|
836
886
|
request: HttpRequest = None,
|
|
837
887
|
instances: models.QuerySet[type["ModelSerializer"] | models.Model] = None,
|
|
838
888
|
query_data: ObjectsQuerySchema = None,
|
|
839
|
-
|
|
889
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
840
890
|
) -> list[dict]:
|
|
841
891
|
"""
|
|
842
892
|
Serialize multiple model instances or fetch and serialize using query parameters.
|
|
@@ -855,8 +905,9 @@ class ModelUtil:
|
|
|
855
905
|
query_data : ObjectsQuerySchema, optional
|
|
856
906
|
Query parameters with filters for multiple object lookup.
|
|
857
907
|
Required when instances is None.
|
|
858
|
-
|
|
859
|
-
|
|
908
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
909
|
+
Purpose of the query, determines which serializable fields to use.
|
|
910
|
+
Defaults to None.
|
|
860
911
|
|
|
861
912
|
Returns
|
|
862
913
|
-------
|
|
@@ -874,7 +925,7 @@ class ModelUtil:
|
|
|
874
925
|
-----
|
|
875
926
|
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
876
927
|
- When instances is provided, request and query_data are ignored
|
|
877
|
-
- Query optimizations applied when
|
|
928
|
+
- Query optimizations applied when is_for is specified
|
|
878
929
|
- Processes queryset asynchronously for efficiency
|
|
879
930
|
"""
|
|
880
931
|
return await self._read_s(
|
|
@@ -882,7 +933,7 @@ class ModelUtil:
|
|
|
882
933
|
request,
|
|
883
934
|
instances,
|
|
884
935
|
query_data,
|
|
885
|
-
|
|
936
|
+
is_for,
|
|
886
937
|
)
|
|
887
938
|
|
|
888
939
|
async def update_s(
|
ninja_aio/schemas/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from .api import (
|
|
|
5
5
|
M2MAddSchemaIn,
|
|
6
6
|
M2MRemoveSchemaIn,
|
|
7
7
|
M2MSchemaIn,
|
|
8
|
+
RelationFilterSchema,
|
|
8
9
|
)
|
|
9
10
|
from .helpers import M2MRelationSchema, QuerySchema, ModelQuerySetSchema, ObjectQuerySchema, ObjectsQuerySchema
|
|
10
11
|
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
|
20
21
|
"ModelQuerySetSchema",
|
|
21
22
|
"ObjectQuerySchema",
|
|
22
23
|
"ObjectsQuerySchema",
|
|
24
|
+
"RelationFilterSchema",
|
|
23
25
|
]
|
ninja_aio/schemas/api.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
from ninja import Schema
|
|
2
4
|
|
|
3
5
|
|
|
@@ -21,4 +23,21 @@ class M2MRemoveSchemaIn(Schema):
|
|
|
21
23
|
|
|
22
24
|
class M2MSchemaIn(Schema):
|
|
23
25
|
add: list = []
|
|
24
|
-
remove: list = []
|
|
26
|
+
remove: list = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RelationFilterSchema(Schema):
|
|
30
|
+
"""
|
|
31
|
+
Schema for configuring relation-based filters in RelationFilterViewSetMixin.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
filter_type: A tuple of (type, default_value) used to generate the query parameter
|
|
35
|
+
field in the filters schema. Example: (int, None) for optional integer filter.
|
|
36
|
+
query_param: The name of the query parameter exposed in the API endpoint.
|
|
37
|
+
This is what clients will use in requests (e.g., ?author_id=5).
|
|
38
|
+
query_filter: The Django ORM lookup to apply (e.g., "author__id", "category__slug").
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
filter_type: tuple[type, Any]
|
|
42
|
+
query_param: str
|
|
43
|
+
query_filter: str
|
ninja_aio/schemas/helpers.py
CHANGED
|
@@ -162,10 +162,12 @@ class QueryUtilBaseScopesSchema(BaseModel):
|
|
|
162
162
|
Schema defining base scopes for query utilities.
|
|
163
163
|
Attributes:
|
|
164
164
|
READ (str): Scope for read operations.
|
|
165
|
+
DETAIL (str): Scope for detail operations.
|
|
165
166
|
QUERYSET_REQUEST (str): Scope for queryset request operations.
|
|
166
167
|
"""
|
|
167
168
|
|
|
168
169
|
READ: str = "read"
|
|
170
|
+
DETAIL: str = "detail"
|
|
169
171
|
QUERYSET_REQUEST: str = "queryset_request"
|
|
170
172
|
|
|
171
173
|
|
ninja_aio/views/api.py
CHANGED
|
@@ -454,11 +454,13 @@ class APIViewSet(API):
|
|
|
454
454
|
qs = await self.model_util.get_objects(
|
|
455
455
|
request,
|
|
456
456
|
query_data=self._get_query_data(),
|
|
457
|
-
|
|
457
|
+
is_for="read",
|
|
458
458
|
)
|
|
459
459
|
if filters is not None:
|
|
460
460
|
qs = await self.query_params_handler(qs, filters.model_dump())
|
|
461
|
-
return await self.model_util.list_read_s(
|
|
461
|
+
return await self.model_util.list_read_s(
|
|
462
|
+
self.schema_out, request, qs, is_for="read"
|
|
463
|
+
)
|
|
462
464
|
|
|
463
465
|
return list
|
|
464
466
|
|
|
@@ -491,6 +493,7 @@ class APIViewSet(API):
|
|
|
491
493
|
query_data=QuerySchema(
|
|
492
494
|
getters={"pk": self._get_pk(pk)}, **query_data.model_dump()
|
|
493
495
|
),
|
|
496
|
+
is_for="detail" if self.schema_detail else "read",
|
|
494
497
|
)
|
|
495
498
|
|
|
496
499
|
return retrieve
|
ninja_aio/views/mixins.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from ninja_aio.views.api import APIViewSet
|
|
2
|
+
from ninja_aio.schemas import RelationFilterSchema
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class IcontainsFilterViewSetMixin(APIViewSet):
|
|
@@ -273,3 +274,65 @@ class LessEqualDateFilterViewSetMixin(DateFilterViewSetMixin):
|
|
|
273
274
|
"""
|
|
274
275
|
|
|
275
276
|
_compare_attr = "__lte"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class RelationFilterViewSetMixin(APIViewSet):
|
|
280
|
+
"""
|
|
281
|
+
Mixin providing filtering for related fields in Django QuerySets.
|
|
282
|
+
|
|
283
|
+
This mixin applies filters on related fields based on configured RelationFilterSchema
|
|
284
|
+
entries. Each entry maps a query parameter name to a Django ORM lookup path.
|
|
285
|
+
|
|
286
|
+
Attributes:
|
|
287
|
+
relations_filters: List of RelationFilterSchema defining the relation filters.
|
|
288
|
+
Each schema specifies:
|
|
289
|
+
- query_param: The API query parameter name (e.g., "author_id")
|
|
290
|
+
- query_filter: The Django ORM lookup (e.g., "author__id")
|
|
291
|
+
- filter_type: Tuple of (type, default) for schema generation
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
class BookViewSet(RelationFilterViewSetMixin, APIViewSet):
|
|
295
|
+
relations_filters = [
|
|
296
|
+
RelationFilterSchema(
|
|
297
|
+
query_param="author_id",
|
|
298
|
+
query_filter="author__id",
|
|
299
|
+
filter_type=(int, None),
|
|
300
|
+
),
|
|
301
|
+
RelationFilterSchema(
|
|
302
|
+
query_param="category_slug",
|
|
303
|
+
query_filter="category__slug",
|
|
304
|
+
filter_type=(str, None),
|
|
305
|
+
),
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
# GET /books?author_id=5 -> queryset.filter(author__id=5)
|
|
309
|
+
# GET /books?category_slug=fiction -> queryset.filter(category__slug="fiction")
|
|
310
|
+
|
|
311
|
+
Notes:
|
|
312
|
+
- Filter values that are None or falsy are skipped.
|
|
313
|
+
- This mixin automatically registers query_params from relations_filters.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
relations_filters: list[RelationFilterSchema] = []
|
|
317
|
+
|
|
318
|
+
def __init_subclass__(cls, **kwargs):
|
|
319
|
+
super().__init_subclass__(**kwargs)
|
|
320
|
+
cls.query_params = {
|
|
321
|
+
**cls.query_params,
|
|
322
|
+
**{
|
|
323
|
+
rel_filter.query_param: rel_filter.filter_type
|
|
324
|
+
for rel_filter in cls.relations_filters
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async def query_params_handler(self, queryset, filters):
|
|
329
|
+
"""
|
|
330
|
+
Apply relation filters to the queryset based on configured relations_filters.
|
|
331
|
+
"""
|
|
332
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
333
|
+
rel_filters = {}
|
|
334
|
+
for rel_filter in self.relations_filters:
|
|
335
|
+
value = filters.get(rel_filter.query_param)
|
|
336
|
+
if value is not None:
|
|
337
|
+
rel_filters[rel_filter.query_filter] = value
|
|
338
|
+
return base_qs.filter(**rel_filters) if rel_filters else base_qs
|
|
File without changes
|
{django_ninja_aio_crud-2.8.0.dist-info → django_ninja_aio_crud-2.10.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|