django-ninja-aio-crud 2.8.0__tar.gz → 2.9.0__tar.gz

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.

Files changed (102) hide show
  1. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/docs.yml +2 -0
  2. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_serializer.md +4 -2
  4. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_util.md +44 -16
  5. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/serializers.md +13 -1
  6. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/__init__.py +1 -1
  7. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/query.py +3 -0
  8. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/serializers.py +19 -9
  9. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/utils.py +115 -64
  10. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/helpers.py +2 -0
  11. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/api.py +5 -2
  12. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/generics/models.py +7 -7
  13. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/models/test_models_extra.py +164 -6
  14. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_serializers.py +10 -4
  15. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/views/test_viewset.py +7 -5
  16. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/.github/dependabot.yml +0 -0
  17. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/coverage.yml +0 -0
  18. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/publish.yml +0 -0
  19. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/.gitignore +0 -0
  20. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/.pre-commit-config.yaml +0 -0
  21. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/LICENSE +0 -0
  22. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/README.md +0 -0
  23. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/CNAME +0 -0
  24. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/authentication.md +0 -0
  25. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/pagination.md +0 -0
  26. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/renderers/orjson_renderer.md +0 -0
  27. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/api_view.md +0 -0
  28. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/api_view_set.md +0 -0
  29. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/decorators.md +0 -0
  30. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/mixins.md +0 -0
  31. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/auth.md +0 -0
  32. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/contributing.md +0 -0
  33. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/extra.css +0 -0
  34. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  35. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  36. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  38. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  39. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  40. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/installation.md +0 -0
  41. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/quick_start.md +0 -0
  42. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/images/bar-swagger.png +0 -0
  43. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/images/favicon.ico +0 -0
  44. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/images/foo-swagger.png +0 -0
  45. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/images/logo.png +0 -0
  46. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  47. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/index.md +0 -0
  48. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/release_notes.md +0 -0
  49. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/requirements.txt +0 -0
  50. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/authentication.md +0 -0
  51. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/crud.md +0 -0
  52. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/filtering.md +0 -0
  53. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/model.md +0 -0
  54. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/main.py +0 -0
  55. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/mkdocs.yml +0 -0
  56. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/api.py +0 -0
  57. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/auth.py +0 -0
  58. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/__init__.py +0 -0
  59. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/operations.py +0 -0
  60. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/views.py +0 -0
  61. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/exceptions.py +0 -0
  62. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/factory/__init__.py +0 -0
  63. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/factory/operations.py +0 -0
  64. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/__init__.py +0 -0
  65. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/api.py +0 -0
  66. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/__init__.py +0 -0
  67. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/parsers.py +0 -0
  68. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/renders.py +0 -0
  69. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/__init__.py +0 -0
  70. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/api.py +0 -0
  71. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/generics.py +0 -0
  72. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/types.py +0 -0
  73. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/__init__.py +0 -0
  74. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/mixins.py +0 -0
  75. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/pyproject.toml +0 -0
  76. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/requirements.dev.txt +0 -0
  77. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/run-local-coverage.sh +0 -0
  78. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/__init__.py +0 -0
  79. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/core/__init__.py +0 -0
  80. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_decorators.py +0 -0
  81. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_exceptions_api.py +0 -0
  82. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_renderer_parser.py +0 -0
  83. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/generics/__init__.py +0 -0
  84. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/generics/literals.py +0 -0
  85. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/generics/request.py +0 -0
  86. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/generics/views.py +0 -0
  87. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/helpers/__init__.py +0 -0
  88. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/helpers/test_many_to_many_api.py +0 -0
  89. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/models/__init__.py +0 -0
  90. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/models/test_model_util.py +0 -0
  91. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/models.py +0 -0
  93. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/schema.py +0 -0
  94. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/serializers.py +0 -0
  95. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/views.py +0 -0
  96. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_auth.py +0 -0
  97. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_decorators.py +0 -0
  98. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_exceptions.py +0 -0
  99. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_query_util.py +0 -0
  100. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/test_settings.py +0 -0
  101. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/views/__init__.py +0 -0
  102. {django_ninja_aio_crud-2.8.0 → django_ninja_aio_crud-2.9.0}/tests/views/test_views.py +0 -0
@@ -20,6 +20,7 @@ on:
20
20
  - "2.6"
21
21
  - "2.6.1"
22
22
  - "2.7.0"
23
+ - "2.8"
23
24
  make_latest:
24
25
  description: 'Set as "latest" and default?'
25
26
  type: boolean
@@ -43,6 +44,7 @@ on:
43
44
  - "2.6"
44
45
  - "2.6.1"
45
46
  - "2.7.0"
47
+ - "2.8"
46
48
  delete_confirm:
47
49
  description: 'Confirm deletion of the selected version'
48
50
  type: boolean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -149,11 +149,13 @@ class User(ModelSerializer):
149
149
 
150
150
  Describes how to build a detail (single object) output schema. Use this when you want the retrieve endpoint to return more fields than the list endpoint.
151
151
 
152
+ **Fallback Behavior:** If `DetailSerializer` is not defined, `generate_detail_s()` automatically falls back to the read schema (same as `generate_read_s()`). This means you only need to define `DetailSerializer` when you want different fields for single-object retrieval vs list views.
153
+
152
154
  **Attributes:**
153
155
 
154
156
  | Attribute | Type | Description |
155
157
  | ----------- | ------------------------ | ----------------------------------------------------------------------------- |
156
- | `fields` | `list[str]` | Model fields to include in detail view |
158
+ | `fields` | `list[str]` | Model fields to include in detail view (falls back to ReadSerializer fields if not defined) |
157
159
  | `excludes` | `list[str]` | Fields to exclude from detail view |
158
160
  | `customs` | `list[tuple]` | Computed fields: `(name, type)` required; `(name, type, default)` optional |
159
161
  | `optionals` | `list[tuple[str, type]]` | Optional output fields |
@@ -280,7 +282,7 @@ class User(ModelSerializer):
280
282
  # Auto-generate schemas
281
283
  UserCreateSchema = User.generate_create_s()
282
284
  UserReadSchema = User.generate_read_s()
283
- UserDetailSchema = User.generate_detail_s() # Returns None if DetailSerializer not defined
285
+ UserDetailSchema = User.generate_detail_s() # Falls back to read schema if DetailSerializer not defined
284
286
  UserUpdateSchema = User.generate_update_s()
285
287
  UserRelatedSchema = User.generate_related_s()
286
288
  ```
@@ -68,7 +68,7 @@ print(util.model_fields)
68
68
 
69
69
  ### `serializable_fields`
70
70
 
71
- Returns serializable fields (ReadSerializer fields or all model fields).
71
+ Returns serializable fields for read operations (ReadSerializer fields or all model fields).
72
72
 
73
73
  ```python
74
74
  class User(ModelSerializer):
@@ -84,6 +84,27 @@ print(util.serializable_fields)
84
84
  # ["id", "username", "email"] (password excluded)
85
85
  ```
86
86
 
87
+ ### `serializable_detail_fields`
88
+
89
+ Returns serializable fields for detail operations (DetailSerializer fields, or falls back to ReadSerializer fields).
90
+
91
+ ```python
92
+ class Article(ModelSerializer):
93
+ title = models.CharField(max_length=200)
94
+ summary = models.TextField()
95
+ content = models.TextField()
96
+
97
+ class ReadSerializer:
98
+ fields = ["id", "title", "summary"]
99
+
100
+ class DetailSerializer:
101
+ fields = ["id", "title", "summary", "content"]
102
+
103
+ util = ModelUtil(Article)
104
+ print(util.serializable_fields) # ["id", "title", "summary"]
105
+ print(util.serializable_detail_fields) # ["id", "title", "summary", "content"]
106
+ ```
107
+
87
108
  ### `model_name`
88
109
 
89
110
  Returns the Django internal model name.
@@ -118,6 +139,10 @@ class Book(ModelSerializer):
118
139
  select_related=["author", "category"],
119
140
  prefetch_related=["tags"],
120
141
  )
142
+ detail = ModelQuerySetSchema(
143
+ select_related=["author", "category", "publisher"],
144
+ prefetch_related=["tags", "reviews"],
145
+ )
121
146
  queryset_request = ModelQuerySetSchema(
122
147
  select_related=[],
123
148
  prefetch_related=["related_items"],
@@ -131,9 +156,10 @@ class Book(ModelSerializer):
131
156
  ]
132
157
  ```
133
158
 
134
- - read: applied to read operations (list/retrieve).
135
- - queryset_request: applied inside queryset_request hook.
136
- - extras: named configurations available via QueryUtil.SCOPES.
159
+ - **read**: applied to list operations (`is_for="read"`).
160
+ - **detail**: applied to retrieve operations (`is_for="detail"`). Falls back to `read` if not defined.
161
+ - **queryset_request**: applied inside queryset_request hook.
162
+ - **extras**: named configurations available via QueryUtil.SCOPES.
137
163
 
138
164
  ## QueryUtil
139
165
 
@@ -178,7 +204,7 @@ qs = await ModelUtil(Book).get_objects(
178
204
  prefetch_related=["tags"],
179
205
  ),
180
206
  with_qs_request=True, # Apply queryset_request hook
181
- is_for_read=True, # union with auto-discovered relations
207
+ is_for="read", # union with auto-discovered relations for read
182
208
  )
183
209
  ```
184
210
 
@@ -187,7 +213,7 @@ qs = await ModelUtil(Book).get_objects(
187
213
  - `request` (`HttpRequest`): Current HTTP request
188
214
  - `query_data` (`ObjectsQuerySchema | None`): Query configuration (filters, select_related, prefetch_related)
189
215
  - `with_qs_request` (`bool`): Apply queryset_request hook if available (default: True)
190
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
216
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query, determines which serializable fields to use for optimization. Use `"read"` for list operations, `"detail"` for retrieve operations. If `None`, only query_data optimizations are applied. (default: None)
191
217
 
192
218
  **Returns:** Optimized `QuerySet`
193
219
 
@@ -198,13 +224,13 @@ Fetch a single object by pk or getters with optimizations:
198
224
  ```python
199
225
  from ninja_aio.schemas.helpers import ObjectQuerySchema, QuerySchema
200
226
 
201
- # by pk + select/prefetch
227
+ # by pk + select/prefetch for detail view
202
228
  obj = await ModelUtil(Book).get_object(
203
229
  request,
204
230
  pk=42,
205
231
  query_data=ObjectQuerySchema(select_related=["author"]),
206
232
  with_qs_request=True,
207
- is_for_read=True,
233
+ is_for="detail",
208
234
  )
209
235
 
210
236
  # by getters (required if pk omitted)
@@ -220,7 +246,7 @@ obj = await ModelUtil(Book).get_object(
220
246
  - `pk` (`int | str | None`): Primary key value (optional if getters provided)
221
247
  - `query_data` (`ObjectQuerySchema | QuerySchema | None`): Query configuration
222
248
  - `with_qs_request` (`bool`): Apply queryset_request hook if available (default: True)
223
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
249
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query. Use `"detail"` for single object retrieval, `"read"` for list operations. (default: None)
224
250
 
225
251
  **Returns:** Model instance
226
252
 
@@ -235,16 +261,17 @@ Uniform serialization methods that accept either instances or query data:
235
261
 
236
262
  ```python
237
263
  schema = Book.generate_read_s()
264
+ detail_schema = Book.generate_detail_s()
238
265
 
239
266
  # single instance
240
267
  data = await ModelUtil(Book).read_s(schema, request, instance=obj)
241
268
 
242
- # single via getters
269
+ # single via getters (detail view)
243
270
  data = await ModelUtil(Book).read_s(
244
- schema,
271
+ detail_schema,
245
272
  request,
246
273
  query_data=ObjectQuerySchema(getters={"pk": 42}),
247
- is_for_read=True,
274
+ is_for="detail",
248
275
  )
249
276
 
250
277
  # list from queryset
@@ -255,7 +282,7 @@ items = await ModelUtil(Book).list_read_s(
255
282
  schema,
256
283
  request,
257
284
  query_data=ObjectsQuerySchema(filters={"is_published": True}),
258
- is_for_read=True,
285
+ is_for="read",
259
286
  )
260
287
  ```
261
288
 
@@ -265,7 +292,7 @@ items = await ModelUtil(Book).list_read_s(
265
292
  - `request` (`HttpRequest`): Current HTTP request
266
293
  - `instance` (`Model | None`): Model instance to serialize (optional)
267
294
  - `query_data` (`ObjectQuerySchema | QuerySchema | None`): Query configuration for fetching (optional)
268
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
295
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query. Use `"detail"` for single object views, `"read"` for list views. (default: None)
269
296
 
270
297
  **Parameters (list_read_s):**
271
298
 
@@ -273,11 +300,12 @@ items = await ModelUtil(Book).list_read_s(
273
300
  - `request` (`HttpRequest`): Current HTTP request
274
301
  - `instances` (`QuerySet | list[Model] | None`): Instances to serialize (optional)
275
302
  - `query_data` (`ObjectsQuerySchema | None`): Query configuration for fetching (optional)
276
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
303
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query. Typically `"read"` for list views. (default: None)
277
304
 
278
305
  **Behavior:**
279
306
 
280
- - When `is_for_read=True`, select_related and prefetch_related are merged with model-discovered relations
307
+ - When `is_for` is specified, select_related and prefetch_related are merged with model-discovered relations based on the operation type
308
+ - When `is_for="detail"` but no `QuerySet.detail` is configured, falls back to `QuerySet.read` optimizations
281
309
  - Passing `instance`/`instances` skips fetching; passing `query_data` fetches automatically
282
310
  - Either `instance`/`instances` OR `query_data` must be provided, not both
283
311
 
@@ -475,12 +475,19 @@ class ArticleSerializer(serializers.Serializer):
475
475
  schema_out = serializers.SchemaModelConfig(
476
476
  fields=["id", "title", "content", "author", "category"]
477
477
  )
478
+ schema_detail = serializers.SchemaModelConfig(
479
+ fields=["id", "title", "content", "author", "category", "tags", "comments"]
480
+ )
478
481
 
479
482
  class QuerySet:
480
483
  read = ModelQuerySetSchema(
481
484
  select_related=["author", "category"],
482
485
  prefetch_related=["tags"],
483
486
  )
487
+ detail = ModelQuerySetSchema(
488
+ select_related=["author", "category", "author__profile"],
489
+ prefetch_related=["tags", "comments", "comments__author"],
490
+ )
484
491
  queryset_request = ModelQuerySetSchema(
485
492
  select_related=[],
486
493
  prefetch_related=["comments"],
@@ -494,7 +501,12 @@ class ArticleSerializer(serializers.Serializer):
494
501
  ]
495
502
  ```
496
503
 
497
- The QuerySet configuration is used by ModelUtil to automatically optimize database queries during read operations.
504
+ The QuerySet configuration is used by ModelUtil to automatically optimize database queries:
505
+
506
+ - **read**: Applied to list operations (`is_for="read"`)
507
+ - **detail**: Applied to retrieve/detail operations (`is_for="detail"`). Falls back to `read` if not defined.
508
+ - **queryset_request**: Applied inside the `queryset_request` hook
509
+ - **extras**: Named configurations available via `QueryUtil.SCOPES`
498
510
 
499
511
  ## Complete Example
500
512
 
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.8.0"
3
+ __version__ = "2.9.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -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."""
@@ -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: type[S_TYPES]):
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: type[S_TYPES]):
334
+ def get_fields(cls, s_type: S_TYPES):
334
335
  """Return explicit declared fields for the serializer type."""
335
- return cls._get_fields(s_type, "fields")
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
- (ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor),
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
- not isinstance(model, ModelSerializerMeta)
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("get_schema_out_data only supports 'Out' or 'Detail' types")
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: