django-ninja-aio-crud 2.4.0__py3-none-any.whl → 2.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -48,6 +48,7 @@ Provides-Extra: test
48
48
 
49
49
  ## ✨ Features
50
50
 
51
+ - Serializer (Meta-driven) first-class: dynamic schemas for existing Django models without inheriting ModelSerializer
51
52
  - Async CRUD ViewSets (create, list, retrieve, update, delete)
52
53
  - Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
53
54
  - Dynamic query params (runtime schema via `pydantic.create_model`)
@@ -61,17 +62,35 @@ Provides-Extra: test
61
62
 
62
63
  ---
63
64
 
64
- ## 📦 Installation
65
+ ## 🚀 Quick Start (Serializer)
65
66
 
66
- ```bash
67
- pip install django-ninja-aio-crud
67
+ If you already have Django models, start with the Meta-driven Serializer for instant CRUD without changing model base classes.
68
+
69
+ ```python
70
+ from ninja_aio.models import serializers
71
+ from ninja_aio.views import APIViewSet
72
+ from ninja_aio import NinjaAIO
73
+ from . import models
74
+
75
+ class BookSerializer(serializers.Serializer):
76
+ class Meta:
77
+ model = models.Book
78
+ schema_in = serializers.SchemaModelConfig(fields=["title", "published"])
79
+ schema_out = serializers.SchemaModelConfig(fields=["id", "title", "published"])
80
+ schema_update = serializers.SchemaModelConfig(optionals=[("title", str), ("published", bool)])
81
+
82
+ api = NinjaAIO()
83
+
84
+ @api.viewset(models.Book)
85
+ class BookViewSet(APIViewSet):
86
+ serializer_class = BookSerializer
68
87
  ```
69
88
 
70
- Add to your project’s dependencies and ensure Django Ninja is installed.
89
+ Visit `/docs` CRUD endpoints ready.
71
90
 
72
91
  ---
73
92
 
74
- ## 🚀 Quick Start
93
+ ## 🚀 Quick Start (ModelSerializer)
75
94
 
76
95
  models.py
77
96
 
@@ -264,29 +283,7 @@ class BookViewSet(APIViewSet):
264
283
 
265
284
  ## Meta-driven Serializer (for vanilla Django models)
266
285
 
267
- If you already have Django models and don't want to inherit from ModelSerializer, use the Meta-driven Serializer to generate dynamic schemas and integrate with APIViewSet.
268
-
269
- Example:
270
-
271
- ```python
272
- from ninja_aio.models import serializers
273
- from . import models
274
-
275
- class BookSerializer(serializers.Serializer):
276
- class Meta:
277
- model = models.Book
278
- schema_in = serializers.SchemaModelConfig(fields=["title", "published"])
279
- schema_out = serializers.SchemaModelConfig(fields=["id", "title", "published"])
280
- schema_update = serializers.SchemaModelConfig(optionals=[("title", str), ("published", bool)])
281
-
282
- @api.viewset(models.Book)
283
- class BookViewSet(APIViewSet):
284
- serializer_class = BookSerializer
285
- ```
286
-
287
- - Works without modifying existing models
288
- - Supports nested relations via relations_serializers
289
- - APIViewSet will auto-generate missing schemas from the serializer
286
+ Moved above as the primary quick start.
290
287
 
291
288
  ---
292
289
 
@@ -1,4 +1,4 @@
1
- ninja_aio/__init__.py,sha256=Ms2GNzuORIvMqPlxF2wzxTHgoQBL_OpaeqByF4LtwhI,119
1
+ ninja_aio/__init__.py,sha256=LBFfoQaFXisUD0DcYa1xZV-zyiAJqx_3QAY-hyxB6OA,119
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=YMzuZ4-ZpUrJBQIabE26gb_GYwsH2rVosWRE95YfdPQ,20775
15
- ninja_aio/helpers/query.py,sha256=YJMdEonCuqx1XjmszCK74mg5hcUPh84ynXrsuoSQdNA,4519
15
+ ninja_aio/helpers/query.py,sha256=lzaH-htswoJVRT-W736HGMkpMba1VmN98TBLv5cZx9Q,4549
16
16
  ninja_aio/models/__init__.py,sha256=L3UQnQAlKoI3F7jinadL-Nn55hkPvnSRPYW0JtnbWFo,114
17
- ninja_aio/models/serializers.py,sha256=wFEG6QrOPN4TiDEIRkOxExIKkjAQ0I432czaELZZxdI,25110
18
- ninja_aio/models/utils.py,sha256=PkvdByNuZKnTzQhdkbqZuG6CEIMIZTCO4QkCUgvqBjs,28776
17
+ ninja_aio/models/serializers.py,sha256=-6NsyTJBdUQl3fkAusC7Kx_2keMRV7CNI9yAlH_fQns,29340
18
+ ninja_aio/models/utils.py,sha256=P-YfbVyzUfxm_s1BrgSd6Zs0HIGdZ79PU1qM0Ud9-Xs,30492
19
19
  ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
20
20
  ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
21
21
  ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
22
22
  ninja_aio/schemas/helpers.py,sha256=W6IeHi5Tmbjh3FXwDYqjqlLBTVj5uTYq3_JVkNUWayo,7355
23
23
  ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
24
- ninja_aio/views/api.py,sha256=AQWtu8oI9R0blk-PW8BpwXLuejj1Z8vqpoNqlU_WrGs,20953
24
+ ninja_aio/views/api.py,sha256=xdzxM1XOPTZmGrRxlIz1QH4UKgxRfg9ls-geatTJrXI,21179
25
25
  ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
26
- django_ninja_aio_crud-2.4.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
27
- django_ninja_aio_crud-2.4.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
28
- django_ninja_aio_crud-2.4.0.dist-info/METADATA,sha256=qRTfA6me4WsclgqfdyVKL7nPOfv3rRasiLNQdVUeZpM,9988
29
- django_ninja_aio_crud-2.4.0.dist-info/RECORD,,
26
+ django_ninja_aio_crud-2.5.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
27
+ django_ninja_aio_crud-2.5.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
28
+ django_ninja_aio_crud-2.5.0.dist-info/METADATA,sha256=Gyk2TCPm3oQaT9YsZrXC7btzZzmDmntKyk_wz0M6Hfg,9963
29
+ django_ninja_aio_crud-2.5.0.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.4.0"
3
+ __version__ = "2.5.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,4 +1,4 @@
1
- from ninja_aio.types import ModelSerializerMeta
1
+ from ninja_aio.models.serializers import Serializer, ModelSerializer
2
2
  from ninja_aio.schemas.helpers import (
3
3
  ModelQuerySetSchema,
4
4
  QueryUtilBaseScopesSchema,
@@ -52,7 +52,7 @@ class QueryUtil:
52
52
 
53
53
  SCOPES: QueryUtilBaseScopesSchema
54
54
 
55
- def __init__(self, model: ModelSerializerMeta):
55
+ def __init__(self, model: ModelSerializer | Serializer):
56
56
  """Initialize QueryUtil, resolving base and extra scope configurations for a model."""
57
57
  self.model = model
58
58
  self._configuration = getattr(self.model, "QuerySet", None)
@@ -1,4 +1,4 @@
1
- from typing import Any, ClassVar, List, Optional
1
+ from typing import Any, List, Optional
2
2
  import warnings
3
3
 
4
4
  from django.conf import settings
@@ -19,7 +19,6 @@ from ninja_aio.schemas.helpers import (
19
19
  ModelQuerySetSchema,
20
20
  ModelQuerySetExtraSchema,
21
21
  )
22
- from ninja_aio.helpers.query import QueryUtil
23
22
 
24
23
 
25
24
  class BaseSerializer:
@@ -36,6 +35,28 @@ class BaseSerializer:
36
35
  - _get_relations_serializers(): optional mapping of relation field -> serializer (may be empty)
37
36
  """
38
37
 
38
+ class QuerySet:
39
+ """
40
+ Configuration container describing how to build query schemas for a model.
41
+ Purpose
42
+ -------
43
+ Describes which fields and extras are available when querying for model
44
+ instances. A factory/metaclass can read this configuration to generate
45
+ Pydantic / Ninja query schemas.
46
+ Attributes
47
+ ----------
48
+ read : ModelQuerySetSchema
49
+ Schema configuration for read operations.
50
+ queryset_request : ModelQuerySetSchema
51
+ Schema configuration for queryset_request hook.
52
+ extras : list[ModelQuerySetExtraSchema]
53
+ Additional computed / synthetic query parameters.
54
+ """
55
+
56
+ read = ModelQuerySetSchema()
57
+ queryset_request = ModelQuerySetSchema()
58
+ extras: list[ModelQuerySetExtraSchema] = []
59
+
39
60
  @classmethod
40
61
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
41
62
  # Subclasses provide implementation.
@@ -199,9 +220,10 @@ class BaseSerializer:
199
220
  reverse_rels: list[tuple] = []
200
221
  rels: list[tuple] = []
201
222
  relations_serializers = cls._get_relations_serializers() or {}
223
+ model = cls._get_model()
202
224
 
203
225
  for f in cls.get_fields("read"):
204
- field_obj = getattr(cls._get_model(), f)
226
+ field_obj = getattr(model, f)
205
227
  is_reverse = isinstance(
206
228
  field_obj,
207
229
  (
@@ -217,7 +239,7 @@ class BaseSerializer:
217
239
  # If explicit relation serializers are declared, require mapping presence.
218
240
  if (
219
241
  is_reverse
220
- and not isinstance(cls._get_model(), ModelSerializerMeta)
242
+ and not isinstance(model, ModelSerializerMeta)
221
243
  and f not in relations_serializers
222
244
  and not getattr(settings, "NINJA_AIO_TESTING", False)
223
245
  ):
@@ -393,41 +415,17 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
393
415
  lifecycle hooks and query utilities.
394
416
  """
395
417
 
396
- util: ClassVar
397
- query_util: ClassVar[QueryUtil]
398
-
399
- class Meta:
400
- abstract = True
401
-
402
418
  def __init_subclass__(cls, **kwargs):
403
419
  super().__init_subclass__(**kwargs)
404
420
  from ninja_aio.models.utils import ModelUtil
421
+ from ninja_aio.helpers.query import QueryUtil
405
422
 
406
423
  # Bind a ModelUtil instance to the subclass for convenient access
407
424
  cls.util = ModelUtil(cls)
408
425
  cls.query_util = QueryUtil(cls)
409
426
 
410
- class QuerySet:
411
- """
412
- Configuration container describing how to build query schemas for a model.
413
- Purpose
414
- -------
415
- Describes which fields and extras are available when querying for model
416
- instances. A factory/metaclass can read this configuration to generate
417
- Pydantic / Ninja query schemas.
418
- Attributes
419
- ----------
420
- read : ModelQuerySetSchema
421
- Schema configuration for read operations.
422
- queryset_request : ModelQuerySetSchema
423
- Schema configuration for queryset_request hook.
424
- extras : list[ModelQuerySetExtraSchema]
425
- Additional computed / synthetic query parameters.
426
- """
427
-
428
- read = ModelQuerySetSchema()
429
- queryset_request = ModelQuerySetSchema()
430
- extras: list[ModelQuerySetExtraSchema] = []
427
+ class Meta:
428
+ abstract = True
431
429
 
432
430
  class CreateSerializer:
433
431
  """Configuration container describing how to build a create (input) schema for a model.
@@ -674,20 +672,26 @@ class Serializer(BaseSerializer):
674
672
  schema components during read schema generation.
675
673
  """
676
674
 
675
+ def __init_subclass__(cls, **kwargs):
676
+ super().__init_subclass__(**kwargs)
677
+ from ninja_aio.models.utils import ModelUtil
678
+ from ninja_aio.helpers.query import QueryUtil
679
+
680
+ cls.model = cls._get_model()
681
+ cls.schema_in = cls.generate_create_s()
682
+ cls.schema_out = cls.generate_read_s()
683
+ cls.schema_update = cls.generate_update_s()
684
+ cls.schema_related = cls.generate_related_s()
685
+ cls.util = ModelUtil(cls._get_model(), serializer_class=cls)
686
+ cls.query_util = QueryUtil(cls)
687
+
677
688
  class Meta:
678
689
  model: models.Model = None
679
- schema_in: SchemaModelConfig = None
680
- schema_out: SchemaModelConfig = None
681
- schema_update: SchemaModelConfig = None
690
+ schema_in: Optional[SchemaModelConfig] = None
691
+ schema_out: Optional[SchemaModelConfig] = None
692
+ schema_update: Optional[SchemaModelConfig] = None
682
693
  relations_serializers: dict[str, "Serializer"] = {}
683
694
 
684
- def __init__(self):
685
- self.model = self._validate_model()
686
- self.schema_in = self.generate_create_s()
687
- self.schema_out = self.generate_read_s()
688
- self.schema_update = self.generate_update_s()
689
- self.schema_related = self.generate_related_s()
690
-
691
695
  @classmethod
692
696
  def _get_meta_data(cls, attr_name: str) -> Any:
693
697
  return getattr(cls.Meta, attr_name, None)
@@ -702,7 +706,7 @@ class Serializer(BaseSerializer):
702
706
  return relations_serializers or {}
703
707
 
704
708
  @classmethod
705
- def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig:
709
+ def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig | None:
706
710
  match schema_type:
707
711
  case "in":
708
712
  return cls._get_meta_data("schema_in")
@@ -710,7 +714,8 @@ class Serializer(BaseSerializer):
710
714
  return cls._get_meta_data("schema_out")
711
715
  case "update":
712
716
  return cls._get_meta_data("schema_update")
713
- return None
717
+ case _:
718
+ return None
714
719
 
715
720
  @classmethod
716
721
  def _validate_model(cls):
@@ -733,6 +738,151 @@ class Serializer(BaseSerializer):
733
738
  return []
734
739
  return getattr(schema, f_type, []) or []
735
740
 
741
+
736
742
  @classmethod
737
743
  async def queryset_request(cls, request: HttpRequest):
738
- return cls._get_model()._default_manager.all()
744
+ return cls.query_util.apply_queryset_optimizations(
745
+ queryset=cls.model._default_manager.all(),
746
+ scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
747
+ )
748
+
749
+ async def post_create(self, instance: models.Model) -> None:
750
+ """
751
+ Async hook executed after first persistence (create path).
752
+ """
753
+ pass
754
+
755
+ async def custom_actions(self, payload: dict[str, Any], instance: models.Model):
756
+ """
757
+ Async hook for reacting to provided custom (synthetic) fields.
758
+
759
+ Parameters
760
+ ----------
761
+ payload : dict
762
+ Custom field name/value pairs.
763
+ """
764
+ pass
765
+
766
+ async def save(self, instance: models.Model) -> models.Model:
767
+ """
768
+ Async helper to save a model instance with lifecycle hooks.
769
+
770
+ Parameters
771
+ ----------
772
+ instance : models.Model
773
+ The model instance to save.
774
+ """
775
+ creation = instance._state.adding
776
+ if creation:
777
+ self.on_create_before_save(instance)
778
+ self.before_save(instance)
779
+ await instance.asave()
780
+ if creation:
781
+ self.on_create_after_save(instance)
782
+ self.after_save(instance)
783
+ return instance
784
+
785
+ async def create(self, payload: dict[str, Any]) -> models.Model:
786
+ """
787
+ Create a new model instance from the provided payload.
788
+
789
+ Parameters
790
+ ----------
791
+ payload : dict
792
+ Input data.
793
+
794
+ Returns
795
+ -------
796
+ models.Model
797
+ Created model instance.
798
+ """
799
+ instance: models.Model = self.model(**payload)
800
+ return await self.save(instance)
801
+
802
+ async def update(
803
+ self, instance: models.Model, payload: dict[str, Any]
804
+ ) -> models.Model:
805
+ """
806
+ Update an existing model instance with the provided payload.
807
+
808
+ Parameters
809
+ ----------
810
+ instance : models.Model
811
+ The model instance to update.
812
+ payload : dict
813
+ Input data.
814
+
815
+ Returns
816
+ -------
817
+ models.Model
818
+ Updated model instance.
819
+ """
820
+ for attr, value in payload.items():
821
+ setattr(instance, attr, value)
822
+ return await self.save(instance)
823
+
824
+ async def model_dump(self, instance: models.Model) -> dict[str, Any]:
825
+ """
826
+ Serialize a model instance to a dictionary using the Out schema.
827
+
828
+ Parameters
829
+ ----------
830
+ instance : models.Model
831
+ The model instance to serialize.
832
+
833
+ Returns
834
+ -------
835
+ dict
836
+ Serialized data.
837
+ """
838
+ return await self.model_util.read_s(schema=self.schema_out, instance=instance)
839
+
840
+ async def models_dump(
841
+ self, instances: models.QuerySet[models.Model]
842
+ ) -> list[dict[str, Any]]:
843
+ """
844
+ Serialize a list of model instances to a list of dictionaries using the Out schema.
845
+
846
+ Parameters
847
+ ----------
848
+ instances : list[models.Model]
849
+ The list of model instances to serialize.
850
+
851
+ Returns
852
+ -------
853
+ list[dict]
854
+ List of serialized data.
855
+ """
856
+ return await self.model_util.list_read_s(
857
+ schema=self.schema_out, instances=instances
858
+ )
859
+
860
+ def after_save(self, instance: models.Model):
861
+ """
862
+ Sync hook executed after any save (create or update).
863
+ """
864
+ pass
865
+
866
+ def before_save(self, instance: models.Model):
867
+ """
868
+ Sync hook executed before any save (create or update).
869
+ """
870
+ pass
871
+
872
+ def on_create_after_save(self, instance: models.Model):
873
+ """
874
+ Sync hook executed only after initial creation save.
875
+ """
876
+ pass
877
+
878
+ def on_create_before_save(self, instance: models.Model):
879
+ """
880
+ Sync hook executed only before initial creation save.
881
+ """
882
+ pass
883
+
884
+ def on_delete(self, instance: models.Model):
885
+ """
886
+ Sync hook executed after delete.
887
+ """
888
+ pass
ninja_aio/models/utils.py CHANGED
@@ -22,6 +22,7 @@ from ninja_aio.exceptions import SerializeError, NotFoundError
22
22
  from ninja_aio.models.serializers import ModelSerializer
23
23
  from ninja_aio.types import ModelSerializerMeta
24
24
  from ninja_aio.schemas.helpers import (
25
+ ModelQuerySetSchema,
25
26
  QuerySchema,
26
27
  ObjectQuerySchema,
27
28
  ObjectsQuerySchema,
@@ -112,6 +113,18 @@ class ModelUtil:
112
113
  raise ConfigError(
113
114
  "ModelUtil cannot accept both model and serializer_class if the model is a ModelSerializer."
114
115
  )
116
+ self.serializer: Serializer = serializer_class() if serializer_class else None
117
+
118
+ @property
119
+ def with_serializer(self) -> bool:
120
+ """
121
+ Indicates if a serializer_class is associated.
122
+
123
+ Returns
124
+ -------
125
+ bool
126
+ """
127
+ return self.serializer_class is not None
115
128
 
116
129
  @property
117
130
  def pk_field_type(self):
@@ -445,6 +458,23 @@ class ModelUtil:
445
458
 
446
459
  return queryset
447
460
 
461
+ def _get_read_optimizations(self) -> ModelQuerySetSchema:
462
+ """
463
+ Retrieve read optimizations from model or serializer class.
464
+
465
+ Returns
466
+ -------
467
+ ModelQuerySetSchema
468
+ Read optimization configuration.
469
+ """
470
+ if isinstance(self.model, ModelSerializerMeta):
471
+ return getattr(self.model.QuerySet, "read", ModelQuerySetSchema())
472
+ if self.with_serializer:
473
+ return getattr(
474
+ self.serializer_class.QuerySet, "read", ModelQuerySetSchema()
475
+ )
476
+ return ModelQuerySetSchema()
477
+
448
478
  def get_reverse_relations(self) -> list[str]:
449
479
  """
450
480
  Discover reverse relation names for safe prefetching.
@@ -454,7 +484,9 @@ class ModelUtil:
454
484
  list[str]
455
485
  Relation attribute names.
456
486
  """
457
- reverse_rels = []
487
+ reverse_rels = self._get_read_optimizations().prefetch_related.copy()
488
+ if reverse_rels:
489
+ return reverse_rels
458
490
  for f in self.serializable_fields:
459
491
  field_obj = getattr(self.model, f)
460
492
  if isinstance(field_obj, ManyToManyDescriptor):
@@ -476,7 +508,9 @@ class ModelUtil:
476
508
  list[str]
477
509
  Relation attribute names.
478
510
  """
479
- select_rels = []
511
+ select_rels = self._get_read_optimizations().select_related.copy()
512
+ if select_rels:
513
+ return select_rels
480
514
  for f in self.serializable_fields:
481
515
  field_obj = getattr(self.model, f)
482
516
  if isinstance(field_obj, ForwardManyToOneDescriptor):
@@ -615,7 +649,8 @@ class ModelUtil:
615
649
  """
616
650
  payload = data.model_dump(mode="json")
617
651
 
618
- is_serializer = isinstance(self.model, ModelSerializerMeta)
652
+ is_serializer = isinstance(self.model, ModelSerializerMeta) or self.with_serializer
653
+ serializer = self.serializer if self.with_serializer else self.model
619
654
 
620
655
  # Collect custom and optional fields (only if ModelSerializerMeta)
621
656
  customs: dict[str, Any] = {}
@@ -624,10 +659,10 @@ class ModelUtil:
624
659
  customs = {
625
660
  k: v
626
661
  for k, v in payload.items()
627
- if self.model.is_custom(k) and k not in self.model_fields
662
+ if serializer.is_custom(k) and k not in self.model_fields
628
663
  }
629
664
  optionals = [
630
- k for k, v in payload.items() if self.model.is_optional(k) and v is None
665
+ k for k, v in payload.items() if serializer.is_optional(k) and v is None
631
666
  ]
632
667
 
633
668
  skip_keys = set()
@@ -636,8 +671,8 @@ class ModelUtil:
636
671
  skip_keys = {
637
672
  k
638
673
  for k, v in payload.items()
639
- if (self.model.is_custom(k) and k not in self.model_fields)
640
- or (self.model.is_optional(k) and v is None)
674
+ if (serializer.is_custom(k) and k not in self.model_fields)
675
+ or (serializer.is_optional(k) and v is None)
641
676
  }
642
677
 
643
678
  # Process payload fields
@@ -674,10 +709,19 @@ class ModelUtil:
674
709
  Serialized created object.
675
710
  """
676
711
  payload, customs = await self.parse_input_data(request, data)
677
- pk = (await self.model.objects.acreate(**payload)).pk
712
+ pk = (
713
+ (await self.model.objects.acreate(**payload)).pk
714
+ if not self.with_serializer
715
+ else (await self.serializer.create(payload)).pk
716
+ )
678
717
  obj = await self.get_object(request, pk)
679
718
  if isinstance(self.model, ModelSerializerMeta):
680
719
  await asyncio.gather(obj.custom_actions(customs), obj.post_create())
720
+ if self.with_serializer:
721
+ await asyncio.gather(
722
+ self.serializer.custom_actions(customs, obj),
723
+ self.serializer.post_create(obj),
724
+ )
681
725
  return await self.read_s(obj_schema, request, obj)
682
726
 
683
727
  async def _read_s(
@@ -871,7 +915,11 @@ class ModelUtil:
871
915
  setattr(obj, k, v)
872
916
  if isinstance(self.model, ModelSerializerMeta):
873
917
  await obj.custom_actions(customs)
874
- await obj.asave()
918
+ if self.with_serializer:
919
+ await self.serializer.custom_actions(customs, obj)
920
+ await self.serializer.save(obj)
921
+ else:
922
+ await obj.asave()
875
923
  updated_object = await self.get_object(request, pk)
876
924
  return await self.read_s(obj_schema, request, updated_object)
877
925
 
ninja_aio/views/api.py CHANGED
@@ -17,7 +17,7 @@ from ninja_aio.schemas import (
17
17
  )
18
18
  from ninja_aio.helpers.api import ManyToManyAPI
19
19
  from ninja_aio.types import ModelSerializerMeta, VIEW_TYPES
20
- from ninja_aio.decorators import unique_view, decorate_view
20
+ from ninja_aio.decorators import unique_view, decorate_view, aatomic
21
21
  from ninja_aio.models import serializers
22
22
 
23
23
  ERROR_CODES = frozenset({400, 401, 404})
@@ -241,6 +241,8 @@ class APIViewSet(API):
241
241
  m2m_relations: list[M2MRelationSchema] = []
242
242
  m2m_auth: list | None = NOT_SET
243
243
  extra_decorators: DecoratorsSchema = DecoratorsSchema()
244
+ model_verbose_name: str = ""
245
+ model_verbose_name_plural: str = ""
244
246
 
245
247
  def __init__(
246
248
  self,
@@ -260,7 +262,13 @@ class APIViewSet(API):
260
262
  self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
261
263
  self.path_schema = self._generate_path_schema()
262
264
  self.filters_schema = self._generate_filters_schema()
263
- self.model_verbose_name = self.model._meta.verbose_name.capitalize()
265
+ self.model_verbose_name = (
266
+ self.model_verbose_name or self.model._meta.verbose_name.capitalize()
267
+ )
268
+ self.model_verbose_name_plural = (
269
+ self.model_verbose_name_plural
270
+ or self.model._meta.verbose_name_plural.capitalize()
271
+ )
264
272
  self.router_tag = self.router_tag or self.model_verbose_name
265
273
  self.router_tags = self.router_tags or tags or [self.router_tag]
266
274
  self.router = Router(tags=self.router_tags)
@@ -400,11 +408,11 @@ class APIViewSet(API):
400
408
  @self.router.post(
401
409
  self.path,
402
410
  auth=self.post_view_auth(),
403
- summary=f"Create {self.model._meta.verbose_name.capitalize()}",
411
+ summary=f"Create {self.model_verbose_name}",
404
412
  description=self.create_docs,
405
413
  response={201: self.schema_out, self.error_codes: GenericMessageSchema},
406
414
  )
407
- @decorate_view(unique_view(self), *self.extra_decorators.create)
415
+ @decorate_view(aatomic, unique_view(self), *self.extra_decorators.create)
408
416
  async def create(request: HttpRequest, data: self.schema_in): # type: ignore
409
417
  return 201, await self.model_util.create_s(request, data, self.schema_out)
410
418
 
@@ -418,7 +426,7 @@ class APIViewSet(API):
418
426
  @self.router.get(
419
427
  self.get_path,
420
428
  auth=self.get_view_auth(),
421
- summary=f"List {self.model._meta.verbose_name_plural.capitalize()}",
429
+ summary=f"List {self.model_verbose_name_plural}",
422
430
  description=self.list_docs,
423
431
  response={
424
432
  200: List[self.schema_out],
@@ -453,7 +461,7 @@ class APIViewSet(API):
453
461
  @self.router.get(
454
462
  self.get_path_retrieve,
455
463
  auth=self.get_view_auth(),
456
- summary=f"Retrieve {self.model._meta.verbose_name.capitalize()}",
464
+ summary=f"Retrieve {self.model_verbose_name}",
457
465
  description=self.retrieve_docs,
458
466
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
459
467
  )
@@ -478,11 +486,11 @@ class APIViewSet(API):
478
486
  @self.router.patch(
479
487
  self.path_retrieve,
480
488
  auth=self.patch_view_auth(),
481
- summary=f"Update {self.model._meta.verbose_name.capitalize()}",
489
+ summary=f"Update {self.model_verbose_name}",
482
490
  description=self.update_docs,
483
491
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
484
492
  )
485
- @decorate_view(unique_view(self), *self.extra_decorators.update)
493
+ @decorate_view(aatomic, unique_view(self), *self.extra_decorators.update)
486
494
  async def update(
487
495
  request: HttpRequest,
488
496
  data: self.schema_update, # type: ignore
@@ -502,11 +510,11 @@ class APIViewSet(API):
502
510
  @self.router.delete(
503
511
  self.path_retrieve,
504
512
  auth=self.delete_view_auth(),
505
- summary=f"Delete {self.model._meta.verbose_name.capitalize()}",
513
+ summary=f"Delete {self.model_verbose_name}",
506
514
  description=self.delete_docs,
507
515
  response={204: None, self.error_codes: GenericMessageSchema},
508
516
  )
509
- @decorate_view(unique_view(self), *self.extra_decorators.delete)
517
+ @decorate_view(aatomic, unique_view(self), *self.extra_decorators.delete)
510
518
  async def delete(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
511
519
  return 204, await self.model_util.delete_s(request, self._get_pk(pk))
512
520