django-ninja-aio-crud 2.3.2__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,9 +1,9 @@
1
1
  import asyncio
2
2
  import base64
3
- from typing import Any, ClassVar
3
+ from typing import Any
4
4
 
5
5
  from ninja import Schema
6
- from ninja.orm import create_schema, fields
6
+ from ninja.orm import fields
7
7
  from ninja.errors import ConfigError
8
8
 
9
9
  from django.db import models
@@ -19,15 +19,14 @@ from django.db.models.fields.related_descriptors import (
19
19
  )
20
20
 
21
21
  from ninja_aio.exceptions import SerializeError, NotFoundError
22
- from ninja_aio.types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
22
+ from ninja_aio.models.serializers import ModelSerializer
23
+ from ninja_aio.types import ModelSerializerMeta
23
24
  from ninja_aio.schemas.helpers import (
24
25
  ModelQuerySetSchema,
25
- ModelQuerySetExtraSchema,
26
26
  QuerySchema,
27
27
  ObjectQuerySchema,
28
28
  ObjectsQuerySchema,
29
29
  )
30
- from ninja_aio.helpers.query import QueryUtil
31
30
 
32
31
 
33
32
  async def agetattr(obj, name: str, default=None):
@@ -95,7 +94,9 @@ class ModelUtil:
95
94
  - Stateless wrapper; safe per-request instantiation.
96
95
  """
97
96
 
98
- def __init__(self, model: type["ModelSerializer"] | models.Model):
97
+ def __init__(
98
+ self, model: type["ModelSerializer"] | models.Model, serializer_class=None
99
+ ):
99
100
  """
100
101
  Initialize with a Django model or ModelSerializer subclass.
101
102
 
@@ -104,7 +105,26 @@ class ModelUtil:
104
105
  model : Model | ModelSerializerMeta
105
106
  Target model class.
106
107
  """
108
+ from ninja_aio.models.serializers import Serializer
109
+
107
110
  self.model = model
111
+ self.serializer_class: Serializer = serializer_class
112
+ if serializer_class is not None and isinstance(model, ModelSerializerMeta):
113
+ raise ConfigError(
114
+ "ModelUtil cannot accept both model and serializer_class if the model is a ModelSerializer."
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
108
128
 
109
129
  @property
110
130
  def pk_field_type(self):
@@ -242,7 +262,11 @@ class ModelUtil:
242
262
  Optimized and filtered queryset.
243
263
  """
244
264
  # Start with base queryset
245
- obj_qs = self.model.objects.all()
265
+ obj_qs = (
266
+ self.model.objects.all()
267
+ if self.serializer_class is None
268
+ else await self.serializer_class.queryset_request(request)
269
+ )
246
270
 
247
271
  # Apply query optimizations
248
272
  obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for_read)
@@ -434,6 +458,23 @@ class ModelUtil:
434
458
 
435
459
  return queryset
436
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
+
437
478
  def get_reverse_relations(self) -> list[str]:
438
479
  """
439
480
  Discover reverse relation names for safe prefetching.
@@ -443,7 +484,9 @@ class ModelUtil:
443
484
  list[str]
444
485
  Relation attribute names.
445
486
  """
446
- reverse_rels = []
487
+ reverse_rels = self._get_read_optimizations().prefetch_related.copy()
488
+ if reverse_rels:
489
+ return reverse_rels
447
490
  for f in self.serializable_fields:
448
491
  field_obj = getattr(self.model, f)
449
492
  if isinstance(field_obj, ManyToManyDescriptor):
@@ -465,7 +508,9 @@ class ModelUtil:
465
508
  list[str]
466
509
  Relation attribute names.
467
510
  """
468
- select_rels = []
511
+ select_rels = self._get_read_optimizations().select_related.copy()
512
+ if select_rels:
513
+ return select_rels
469
514
  for f in self.serializable_fields:
470
515
  field_obj = getattr(self.model, f)
471
516
  if isinstance(field_obj, ForwardManyToOneDescriptor):
@@ -604,7 +649,8 @@ class ModelUtil:
604
649
  """
605
650
  payload = data.model_dump(mode="json")
606
651
 
607
- 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
608
654
 
609
655
  # Collect custom and optional fields (only if ModelSerializerMeta)
610
656
  customs: dict[str, Any] = {}
@@ -613,10 +659,10 @@ class ModelUtil:
613
659
  customs = {
614
660
  k: v
615
661
  for k, v in payload.items()
616
- 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
617
663
  }
618
664
  optionals = [
619
- 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
620
666
  ]
621
667
 
622
668
  skip_keys = set()
@@ -625,8 +671,8 @@ class ModelUtil:
625
671
  skip_keys = {
626
672
  k
627
673
  for k, v in payload.items()
628
- if (self.model.is_custom(k) and k not in self.model_fields)
629
- 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)
630
676
  }
631
677
 
632
678
  # Process payload fields
@@ -663,10 +709,19 @@ class ModelUtil:
663
709
  Serialized created object.
664
710
  """
665
711
  payload, customs = await self.parse_input_data(request, data)
666
- 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
+ )
667
717
  obj = await self.get_object(request, pk)
668
718
  if isinstance(self.model, ModelSerializerMeta):
669
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
+ )
670
725
  return await self.read_s(obj_schema, request, obj)
671
726
 
672
727
  async def _read_s(
@@ -860,7 +915,11 @@ class ModelUtil:
860
915
  setattr(obj, k, v)
861
916
  if isinstance(self.model, ModelSerializerMeta):
862
917
  await obj.custom_actions(customs)
863
- 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()
864
923
  updated_object = await self.get_object(request, pk)
865
924
  return await self.read_s(obj_schema, request, updated_object)
866
925
 
@@ -881,657 +940,3 @@ class ModelUtil:
881
940
  obj = await self.get_object(request, pk)
882
941
  await obj.adelete()
883
942
  return None
884
-
885
-
886
- class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
887
- """
888
- ModelSerializer
889
- =================
890
- Abstract mixin for Django models centralizing (on the model class itself) the
891
- declarative configuration required to auto-generate create / update / read /
892
- related schemas.
893
-
894
- Goals
895
- -----
896
- - Remove duplication between Model and separate serializer classes.
897
- - Provide clear extension points (sync + async hooks, custom synthetic fields).
898
-
899
- See inline docstrings for per-method behavior.
900
- """
901
-
902
- util: ClassVar[ModelUtil]
903
- query_util: ClassVar[QueryUtil]
904
-
905
- class Meta:
906
- abstract = True
907
-
908
- def __init_subclass__(cls, **kwargs):
909
- super().__init_subclass__(**kwargs)
910
- # Bind a ModelUtil instance to the subclass for convenient access
911
- cls.util = ModelUtil(cls)
912
- cls.query_util = QueryUtil(cls)
913
-
914
- class QuerySet:
915
- """
916
- Configuration container describing how to build query schemas for a model.
917
- Purpose
918
- -------
919
- Describes which fields and extras are available when querying for model
920
- instances. A factory/metaclass can read this configuration to generate
921
- Pydantic / Ninja query schemas.
922
- Attributes
923
- ----------
924
- read : ModelQuerySetSchema
925
- Schema configuration for read operations.
926
- queryset_request : ModelQuerySetSchema
927
- Schema configuration for queryset_request hook.
928
- extras : list[ModelQuerySetExtraSchema]
929
- Additional computed / synthetic query parameters.
930
- """
931
- read = ModelQuerySetSchema()
932
- queryset_request = ModelQuerySetSchema()
933
- extras: list[ModelQuerySetExtraSchema] = []
934
-
935
- class CreateSerializer:
936
- """Configuration container describing how to build a create (input) schema for a model.
937
-
938
- Purpose
939
- -------
940
- Describes which fields are accepted (and in what form) when creating a new
941
- instance. A factory/metaclass can read this configuration to generate a
942
- Pydantic / Ninja input schema.
943
-
944
- Attributes
945
- ----------
946
- fields : list[str]
947
- REQUIRED model fields.
948
- optionals : list[tuple[str, type]]
949
- Optional model fields (nullable / patch-like).
950
- customs : list[tuple[str, type, Any]]
951
- Synthetic input fields (non-model).
952
- excludes : list[str]
953
- Disallowed model fields on create (e.g., id, timestamps).
954
- """
955
-
956
- fields: list[str] = []
957
- customs: list[tuple[str, type, Any]] = []
958
- optionals: list[tuple[str, type]] = []
959
- excludes: list[str] = []
960
-
961
- class ReadSerializer:
962
- """Configuration describing how to build a read (output) schema.
963
-
964
- Attributes
965
- ----------
966
- fields : list[str]
967
- Explicit model fields to include.
968
- excludes : list[str]
969
- Fields to force exclude (safety).
970
- customs : list[tuple[str, type, Any]]
971
- Computed / synthetic output attributes.
972
- """
973
-
974
- fields: list[str] = []
975
- excludes: list[str] = []
976
- customs: list[tuple[str, type, Any]] = []
977
-
978
- class UpdateSerializer:
979
- """Configuration describing update (PATCH/PUT) schema.
980
-
981
- Attributes
982
- ----------
983
- fields : list[str]
984
- Required update fields (rare).
985
- optionals : list[tuple[str, type]]
986
- Editable optional fields.
987
- customs : list[tuple[str, type, Any]]
988
- Synthetic operational inputs.
989
- excludes : list[str]
990
- Immutable / blocked fields.
991
- """
992
-
993
- fields: list[str] = []
994
- customs: list[tuple[str, type, Any]] = []
995
- optionals: list[tuple[str, type]] = []
996
- excludes: list[str] = []
997
-
998
- @classmethod
999
- def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
1000
- """
1001
- Internal accessor for raw configuration lists.
1002
-
1003
- Parameters
1004
- ----------
1005
- s_type : str
1006
- Serializer type ("create" | "update" | "read").
1007
- f_type : str
1008
- Field category ("fields" | "optionals" | "customs" | "excludes").
1009
-
1010
- Returns
1011
- -------
1012
- list
1013
- Raw configuration list or empty list.
1014
- """
1015
- match s_type:
1016
- case "create":
1017
- fields = getattr(cls.CreateSerializer, f_type, [])
1018
- case "update":
1019
- fields = getattr(cls.UpdateSerializer, f_type, [])
1020
- case "read":
1021
- fields = getattr(cls.ReadSerializer, f_type, [])
1022
- return fields
1023
-
1024
- @classmethod
1025
- def _is_special_field(
1026
- cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
1027
- ):
1028
- """
1029
- Determine if a field is declared in a given category for a serializer type.
1030
-
1031
- Parameters
1032
- ----------
1033
- s_type : str
1034
- field : str
1035
- f_type : str
1036
-
1037
- Returns
1038
- -------
1039
- bool
1040
- """
1041
- special_fields = cls._get_fields(s_type, f_type)
1042
- return any(field in special_f for special_f in special_fields)
1043
-
1044
- @classmethod
1045
- def _generate_model_schema(
1046
- cls,
1047
- schema_type: type[SCHEMA_TYPES],
1048
- depth: int = None,
1049
- ) -> Schema:
1050
- """
1051
- Core schema factory bridging configuration and ninja.orm.create_schema.
1052
-
1053
- Parameters
1054
- ----------
1055
- schema_type : str
1056
- "In" | "Patch" | "Out" | "Related".
1057
- depth : int, optional
1058
- Relation depth for read schema.
1059
-
1060
- Returns
1061
- -------
1062
- Schema | None
1063
- Generated schema class or None if no fields.
1064
- """
1065
- match schema_type:
1066
- case "In":
1067
- s_type = "create"
1068
- case "Patch":
1069
- s_type = "update"
1070
- case "Out":
1071
- fields, reverse_rels, excludes, customs = cls.get_schema_out_data()
1072
- if not fields and not reverse_rels and not excludes and not customs:
1073
- return None
1074
- return create_schema(
1075
- model=cls,
1076
- name=f"{cls._meta.model_name}SchemaOut",
1077
- depth=depth,
1078
- fields=fields,
1079
- custom_fields=reverse_rels + customs,
1080
- exclude=excludes,
1081
- )
1082
- case "Related":
1083
- fields, customs = cls.get_related_schema_data()
1084
- if not fields and not customs:
1085
- return None
1086
- return create_schema(
1087
- model=cls,
1088
- name=f"{cls._meta.model_name}SchemaRelated",
1089
- fields=fields,
1090
- custom_fields=customs,
1091
- )
1092
-
1093
- fields = cls.get_fields(s_type)
1094
- optionals = cls.get_optional_fields(s_type)
1095
- customs = cls.get_custom_fields(s_type) + optionals
1096
- excludes = cls.get_excluded_fields(s_type)
1097
- if not fields and not excludes:
1098
- fields = [f[0] for f in optionals]
1099
- return (
1100
- create_schema(
1101
- model=cls,
1102
- name=f"{cls._meta.model_name}Schema{schema_type}",
1103
- fields=fields,
1104
- custom_fields=customs,
1105
- exclude=excludes,
1106
- )
1107
- if fields or customs or excludes
1108
- else None
1109
- )
1110
-
1111
- @classmethod
1112
- def verbose_name_path_resolver(cls) -> str:
1113
- """
1114
- Slugify plural verbose name for URL path segment.
1115
-
1116
- Returns
1117
- -------
1118
- str
1119
- """
1120
- return "-".join(cls._meta.verbose_name_plural.split(" "))
1121
-
1122
- def has_changed(self, field: str) -> bool:
1123
- """
1124
- Check if a model field has changed compared to the persisted value.
1125
-
1126
- Parameters
1127
- ----------
1128
- field : str
1129
- Field name.
1130
-
1131
- Returns
1132
- -------
1133
- bool
1134
- True if in-memory value differs from DB value.
1135
- """
1136
- if not self.pk:
1137
- return False
1138
- old_value = (
1139
- self.__class__._default_manager.filter(pk=self.pk)
1140
- .values(field)
1141
- .get()[field]
1142
- )
1143
- return getattr(self, field) != old_value
1144
-
1145
- @classmethod
1146
- async def queryset_request(cls, request: HttpRequest):
1147
- """
1148
- Override to return a request-scoped filtered queryset.
1149
-
1150
- Parameters
1151
- ----------
1152
- request : HttpRequest
1153
-
1154
- Returns
1155
- -------
1156
- QuerySet
1157
- """
1158
- return cls.query_util.apply_queryset_optimizations(
1159
- queryset=cls.objects.all(),
1160
- scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
1161
- )
1162
-
1163
- async def post_create(self) -> None:
1164
- """
1165
- Async hook executed after first persistence (create path).
1166
- """
1167
- pass
1168
-
1169
- async def custom_actions(self, payload: dict[str, Any]):
1170
- """
1171
- Async hook for reacting to provided custom (synthetic) fields.
1172
-
1173
- Parameters
1174
- ----------
1175
- payload : dict
1176
- Custom field name/value pairs.
1177
- """
1178
- pass
1179
-
1180
- @classmethod
1181
- def get_related_schema_data(cls):
1182
- """
1183
- Build field/custom lists for 'Related' schema (flattening non-relational fields).
1184
-
1185
- Returns
1186
- -------
1187
- tuple[list[str] | None, list[tuple] | None]
1188
- (related_fields, custom_related_fields) or (None, None)
1189
- """
1190
- fields = cls.get_fields("read")
1191
- custom_f = {
1192
- name: (value, default)
1193
- for name, value, default in cls.get_custom_fields("read")
1194
- }
1195
- _related_fields = []
1196
- for f in fields + list(custom_f.keys()):
1197
- field_obj = getattr(cls, f)
1198
- if not isinstance(
1199
- field_obj,
1200
- (
1201
- ManyToManyDescriptor,
1202
- ReverseManyToOneDescriptor,
1203
- ReverseOneToOneDescriptor,
1204
- ForwardManyToOneDescriptor,
1205
- ForwardOneToOneDescriptor,
1206
- ),
1207
- ):
1208
- _related_fields.append(f)
1209
-
1210
- if not _related_fields:
1211
- return None, None
1212
-
1213
- custom_related_fields = [
1214
- (f, *custom_f[f]) for f in _related_fields if f in custom_f
1215
- ]
1216
- related_fields = [f for f in _related_fields if f not in custom_f]
1217
- return related_fields, custom_related_fields
1218
-
1219
- @classmethod
1220
- def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
1221
- """
1222
- Build a reverse relation schema component for 'Out' schema generation.
1223
- """
1224
- if isinstance(descriptor, ManyToManyDescriptor):
1225
- rel_model: ModelSerializer = descriptor.field.related_model
1226
- if descriptor.reverse: # reverse side of M2M
1227
- rel_model = descriptor.field.model
1228
- rel_type = "many"
1229
- elif isinstance(descriptor, ReverseManyToOneDescriptor):
1230
- rel_model = descriptor.field.model
1231
- rel_type = "many"
1232
- else: # ReverseOneToOneDescriptor
1233
- rel_model = descriptor.related.related_model
1234
- rel_type = "one"
1235
-
1236
- if not isinstance(rel_model, ModelSerializerMeta):
1237
- return None
1238
- if not rel_model.get_fields("read") and not rel_model.get_custom_fields("read"):
1239
- return None
1240
-
1241
- rel_schema = (
1242
- rel_model.generate_related_s()
1243
- if rel_type == "one"
1244
- else list[rel_model.generate_related_s()]
1245
- )
1246
- return (field_name, rel_schema | None, None)
1247
-
1248
- @classmethod
1249
- def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
1250
- """
1251
- Build a forward relation schema component for 'Out' schema generation.
1252
- """
1253
- rel_model = descriptor.field.related_model
1254
- if not isinstance(rel_model, ModelSerializerMeta):
1255
- return True # Signal: treat as plain field
1256
- if not rel_model.get_fields("read") and not rel_model.get_custom_fields("read"):
1257
- return None # Skip entirely
1258
- rel_schema = rel_model.generate_related_s()
1259
- return (field_name, rel_schema | None, None)
1260
-
1261
- @classmethod
1262
- def get_schema_out_data(cls):
1263
- """
1264
- Collect components for 'Out' read schema generation.
1265
-
1266
- Returns
1267
- -------
1268
- tuple
1269
- (fields, reverse_rel_descriptors, excludes, custom_fields_with_forward_relations)
1270
- """
1271
-
1272
- fields: list[str] = []
1273
- reverse_rels: list[tuple] = []
1274
- rels: list[tuple] = []
1275
-
1276
- for f in cls.get_fields("read"):
1277
- field_obj = getattr(cls, f)
1278
-
1279
- # Reverse relations
1280
- if isinstance(
1281
- field_obj,
1282
- (
1283
- ManyToManyDescriptor,
1284
- ReverseManyToOneDescriptor,
1285
- ReverseOneToOneDescriptor,
1286
- ),
1287
- ):
1288
- rel_tuple = cls._build_schema_reverse_rel(f, field_obj)
1289
- if rel_tuple:
1290
- reverse_rels.append(rel_tuple)
1291
- continue
1292
-
1293
- # Forward relations
1294
- if isinstance(
1295
- field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
1296
- ):
1297
- rel_tuple = cls._build_schema_forward_rel(f, field_obj)
1298
- if rel_tuple is True:
1299
- fields.append(f)
1300
- elif rel_tuple:
1301
- rels.append(rel_tuple)
1302
- # If rel_tuple is None -> skip
1303
- continue
1304
-
1305
- # Plain field
1306
- fields.append(f)
1307
-
1308
- return (
1309
- fields,
1310
- reverse_rels,
1311
- cls.get_excluded_fields("read"),
1312
- cls.get_custom_fields("read") + rels,
1313
- )
1314
-
1315
- @classmethod
1316
- def is_custom(cls, field: str):
1317
- """
1318
- Check if a field is declared as a custom input (create or update).
1319
-
1320
- Parameters
1321
- ----------
1322
- field : str
1323
-
1324
- Returns
1325
- -------
1326
- bool
1327
- """
1328
- return cls._is_special_field(
1329
- "create", field, "customs"
1330
- ) or cls._is_special_field("update", field, "customs")
1331
-
1332
- @classmethod
1333
- def is_optional(cls, field: str):
1334
- """
1335
- Check if a field is declared as optional (create or update).
1336
-
1337
- Parameters
1338
- ----------
1339
- field : str
1340
-
1341
- Returns
1342
- -------
1343
- bool
1344
- """
1345
- return cls._is_special_field(
1346
- "create", field, "optionals"
1347
- ) or cls._is_special_field("update", field, "optionals")
1348
-
1349
- @classmethod
1350
- def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple[str, type, Any]]:
1351
- """
1352
- Normalize declared custom field specs into (name, py_type, default) triples.
1353
-
1354
- Accepted tuple shapes:
1355
- (name, py_type, default) -> keeps provided default (callable or literal)
1356
- (name, py_type) -> marks as required (default = Ellipsis)
1357
- Any other arity raises ValueError.
1358
-
1359
- Parameters
1360
- ----------
1361
- s_type : str
1362
- "create" | "update" | "read"
1363
-
1364
- Returns
1365
- -------
1366
- list[tuple[str, type, Any]]
1367
- """
1368
- raw_customs = cls._get_fields(s_type, "customs") or []
1369
- normalized: list[tuple[str, type, Any]] = []
1370
- for spec in raw_customs:
1371
- if not isinstance(spec, tuple):
1372
- raise ValueError(f"Custom field spec must be a tuple, got {type(spec)}")
1373
- match len(spec):
1374
- case 3:
1375
- name, py_type, default = spec
1376
- case 2:
1377
- name, py_type = spec
1378
- default = ...
1379
- case _:
1380
- raise ValueError(
1381
- f"Custom field tuple must have length 2 or 3 (name, type[, default]); got {len(spec)}"
1382
- )
1383
- normalized.append((name, py_type, default))
1384
- return normalized
1385
-
1386
- @classmethod
1387
- def get_optional_fields(cls, s_type: type[S_TYPES]):
1388
- """
1389
- Return optional field specifications normalized to (name, type, None).
1390
-
1391
- Parameters
1392
- ----------
1393
- s_type : str
1394
-
1395
- Returns
1396
- -------
1397
- list[tuple[str, type, None]]
1398
- """
1399
- return [
1400
- (field, field_type, None)
1401
- for field, field_type in cls._get_fields(s_type, "optionals")
1402
- ]
1403
-
1404
- @classmethod
1405
- def get_excluded_fields(cls, s_type: type[S_TYPES]):
1406
- """
1407
- Return excluded field names for a serializer type.
1408
-
1409
- Parameters
1410
- ----------
1411
- s_type : str
1412
-
1413
- Returns
1414
- -------
1415
- list[str]
1416
- """
1417
- return cls._get_fields(s_type, "excludes")
1418
-
1419
- @classmethod
1420
- def get_fields(cls, s_type: type[S_TYPES]):
1421
- """
1422
- Return explicit declared fields for a serializer type.
1423
-
1424
- Parameters
1425
- ----------
1426
- s_type : str
1427
-
1428
- Returns
1429
- -------
1430
- list[str]
1431
- """
1432
- return cls._get_fields(s_type, "fields")
1433
-
1434
- @classmethod
1435
- def generate_read_s(cls, depth: int = 1) -> Schema:
1436
- """
1437
- Generate read (Out) schema.
1438
-
1439
- Parameters
1440
- ----------
1441
- depth : int
1442
- Relation depth.
1443
-
1444
- Returns
1445
- -------
1446
- Schema | None
1447
- """
1448
- return cls._generate_model_schema("Out", depth)
1449
-
1450
- @classmethod
1451
- def generate_create_s(cls) -> Schema:
1452
- """
1453
- Generate create (In) schema.
1454
-
1455
- Returns
1456
- -------
1457
- Schema | None
1458
- """
1459
- return cls._generate_model_schema("In")
1460
-
1461
- @classmethod
1462
- def generate_update_s(cls) -> Schema:
1463
- """
1464
- Generate update (Patch) schema.
1465
-
1466
- Returns
1467
- -------
1468
- Schema | None
1469
- """
1470
- return cls._generate_model_schema("Patch")
1471
-
1472
- @classmethod
1473
- def generate_related_s(cls) -> Schema:
1474
- """
1475
- Generate related (nested) schema.
1476
-
1477
- Returns
1478
- -------
1479
- Schema | None
1480
- """
1481
- return cls._generate_model_schema("Related")
1482
-
1483
- def after_save(self):
1484
- """
1485
- Sync hook executed after any save (create or update).
1486
- """
1487
- pass
1488
-
1489
- def before_save(self):
1490
- """
1491
- Sync hook executed before any save (create or update).
1492
- """
1493
- pass
1494
-
1495
- def on_create_after_save(self):
1496
- """
1497
- Sync hook executed only after initial creation save.
1498
- """
1499
- pass
1500
-
1501
- def on_create_before_save(self):
1502
- """
1503
- Sync hook executed only before initial creation save.
1504
- """
1505
- pass
1506
-
1507
- def on_delete(self):
1508
- """
1509
- Sync hook executed after delete.
1510
- """
1511
- pass
1512
-
1513
- def save(self, *args, **kwargs):
1514
- """
1515
- Override save lifecycle to inject create/update hooks.
1516
- """
1517
- state_adding = self._state.adding
1518
- if state_adding:
1519
- self.on_create_before_save()
1520
- self.before_save()
1521
- super().save(*args, **kwargs)
1522
- if state_adding:
1523
- self.on_create_after_save()
1524
- self.after_save()
1525
-
1526
- def delete(self, *args, **kwargs):
1527
- """
1528
- Override delete to inject on_delete hook.
1529
-
1530
- Returns
1531
- -------
1532
- tuple(int, dict)
1533
- Django delete return signature.
1534
- """
1535
- res = super().delete(*args, **kwargs)
1536
- self.on_delete()
1537
- return res