django-ninja-aio-crud 2.7.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.
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/docs.yml +4 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_serializer.md +71 -7
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_util.md +44 -16
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/serializers.md +40 -3
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/api_view_set.md +80 -20
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/api.py +1 -1
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/query.py +3 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/serializers.py +153 -70
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/utils.py +115 -64
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/helpers.py +34 -6
- django_ninja_aio_crud-2.9.0/ninja_aio/types.py +24 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/api.py +26 -9
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/models.py +7 -7
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/views.py +1 -1
- django_ninja_aio_crud-2.9.0/tests/helpers/test_many_to_many_api.py +282 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/models/test_models_extra.py +164 -6
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/models.py +13 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_serializers.py +127 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/views/test_viewset.py +143 -0
- django_ninja_aio_crud-2.7.0/ninja_aio/types.py +0 -19
- django_ninja_aio_crud-2.7.0/tests/helpers/test_many_to_many_api.py +0 -118
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/README.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/main.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/views/test_views.py +0 -0
|
@@ -19,6 +19,8 @@ on:
|
|
|
19
19
|
- "2.5"
|
|
20
20
|
- "2.6"
|
|
21
21
|
- "2.6.1"
|
|
22
|
+
- "2.7.0"
|
|
23
|
+
- "2.8"
|
|
22
24
|
make_latest:
|
|
23
25
|
description: 'Set as "latest" and default?'
|
|
24
26
|
type: boolean
|
|
@@ -41,6 +43,8 @@ on:
|
|
|
41
43
|
- "2.5"
|
|
42
44
|
- "2.6"
|
|
43
45
|
- "2.6.1"
|
|
46
|
+
- "2.7.0"
|
|
47
|
+
- "2.8"
|
|
44
48
|
delete_confirm:
|
|
45
49
|
description: 'Confirm deletion of the selected version'
|
|
46
50
|
type: boolean
|
{django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_serializer.md
RENAMED
|
@@ -145,6 +145,68 @@ class User(ModelSerializer):
|
|
|
145
145
|
}
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
### DetailSerializer
|
|
149
|
+
|
|
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
|
+
|
|
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
|
+
|
|
154
|
+
**Attributes:**
|
|
155
|
+
|
|
156
|
+
| Attribute | Type | Description |
|
|
157
|
+
| ----------- | ------------------------ | ----------------------------------------------------------------------------- |
|
|
158
|
+
| `fields` | `list[str]` | Model fields to include in detail view (falls back to ReadSerializer fields if not defined) |
|
|
159
|
+
| `excludes` | `list[str]` | Fields to exclude from detail view |
|
|
160
|
+
| `customs` | `list[tuple]` | Computed fields: `(name, type)` required; `(name, type, default)` optional |
|
|
161
|
+
| `optionals` | `list[tuple[str, type]]` | Optional output fields |
|
|
162
|
+
|
|
163
|
+
**Example:**
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
class Article(ModelSerializer):
|
|
167
|
+
title = models.CharField(max_length=200)
|
|
168
|
+
summary = models.TextField()
|
|
169
|
+
content = models.TextField()
|
|
170
|
+
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
171
|
+
tags = models.ManyToManyField(Tag)
|
|
172
|
+
view_count = models.IntegerField(default=0)
|
|
173
|
+
|
|
174
|
+
class ReadSerializer:
|
|
175
|
+
# List view: minimal fields for performance
|
|
176
|
+
fields = ["id", "title", "summary", "author"]
|
|
177
|
+
|
|
178
|
+
class DetailSerializer:
|
|
179
|
+
# Detail view: all fields including expensive relations
|
|
180
|
+
fields = ["id", "title", "summary", "content", "author", "tags", "view_count"]
|
|
181
|
+
customs = [
|
|
182
|
+
("reading_time", int, lambda obj: len(obj.content.split()) // 200),
|
|
183
|
+
]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Generated Output (List):**
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
[
|
|
190
|
+
{"id": 1, "title": "Getting Started", "summary": "...", "author": {...}},
|
|
191
|
+
{"id": 2, "title": "Advanced Topics", "summary": "...", "author": {...}}
|
|
192
|
+
]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Generated Output (Detail):**
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"id": 1,
|
|
200
|
+
"title": "Getting Started",
|
|
201
|
+
"summary": "...",
|
|
202
|
+
"content": "Full article content here...",
|
|
203
|
+
"author": {...},
|
|
204
|
+
"tags": [{"id": 1, "name": "python"}, {"id": 2, "name": "django"}],
|
|
205
|
+
"view_count": 1234,
|
|
206
|
+
"reading_time": 5
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
148
210
|
### UpdateSerializer
|
|
149
211
|
|
|
150
212
|
Describes how to build an update (partial/full) input schema.
|
|
@@ -194,14 +256,15 @@ class User(ModelSerializer):
|
|
|
194
256
|
|
|
195
257
|
### Auto-Generated Schemas
|
|
196
258
|
|
|
197
|
-
ModelSerializer automatically generates
|
|
259
|
+
ModelSerializer automatically generates five schema types:
|
|
198
260
|
|
|
199
|
-
| Method
|
|
200
|
-
|
|
|
201
|
-
| `generate_create_s()`
|
|
202
|
-
| `generate_update_s()`
|
|
203
|
-
| `generate_read_s(depth=1)`
|
|
204
|
-
| `
|
|
261
|
+
| Method | Schema Type | Purpose |
|
|
262
|
+
| ---------------------------- | ------------------ | ------------------------------------ |
|
|
263
|
+
| `generate_create_s()` | Input ("In") | POST endpoint payload |
|
|
264
|
+
| `generate_update_s()` | Input ("Patch") | PATCH/PUT endpoint payload |
|
|
265
|
+
| `generate_read_s(depth=1)` | Output ("Out") | List response with nested relations |
|
|
266
|
+
| `generate_detail_s(depth=1)` | Output ("Detail") | Single object response (retrieve) |
|
|
267
|
+
| `generate_related_s()` | Output ("Related") | Compact nested representation |
|
|
205
268
|
|
|
206
269
|
**Example:**
|
|
207
270
|
|
|
@@ -219,6 +282,7 @@ class User(ModelSerializer):
|
|
|
219
282
|
# Auto-generate schemas
|
|
220
283
|
UserCreateSchema = User.generate_create_s()
|
|
221
284
|
UserReadSchema = User.generate_read_s()
|
|
285
|
+
UserDetailSchema = User.generate_detail_s() # Falls back to read schema if DetailSerializer not defined
|
|
222
286
|
UserUpdateSchema = User.generate_update_s()
|
|
223
287
|
UserRelatedSchema = User.generate_related_s()
|
|
224
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
|
|
135
|
-
-
|
|
136
|
-
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
271
|
+
detail_schema,
|
|
245
272
|
request,
|
|
246
273
|
query_data=ObjectQuerySchema(getters={"pk": 42}),
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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 `
|
|
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
|
|
|
@@ -37,7 +37,8 @@ Define a Serializer subclass with a nested Meta:
|
|
|
37
37
|
|
|
38
38
|
- **model**: Django model class
|
|
39
39
|
- **schema_in**: SchemaModelConfig for create inputs
|
|
40
|
-
- **schema_out**: SchemaModelConfig for read outputs
|
|
40
|
+
- **schema_out**: SchemaModelConfig for read outputs (list endpoint)
|
|
41
|
+
- **schema_detail**: SchemaModelConfig for detail outputs (retrieve endpoint)
|
|
41
42
|
- **schema_update**: SchemaModelConfig for patch/update inputs
|
|
42
43
|
- **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
|
|
43
44
|
|
|
@@ -55,13 +56,37 @@ Generate schemas explicitly using these methods:
|
|
|
55
56
|
```python
|
|
56
57
|
# Explicitly generate schemas when needed
|
|
57
58
|
ArticleSerializer.generate_create_s() # Returns create (In) schema
|
|
58
|
-
ArticleSerializer.generate_read_s() # Returns read (Out) schema
|
|
59
|
+
ArticleSerializer.generate_read_s() # Returns read (Out) schema for list endpoint
|
|
60
|
+
ArticleSerializer.generate_detail_s() # Returns detail (Out) schema for retrieve endpoint
|
|
59
61
|
ArticleSerializer.generate_update_s() # Returns update (Patch) schema
|
|
60
62
|
ArticleSerializer.generate_related_s() # Returns related (nested) schema
|
|
61
63
|
```
|
|
62
64
|
|
|
63
65
|
Schemas support **forward references and circular dependencies** via string references in `relations_serializers`.
|
|
64
66
|
|
|
67
|
+
### Detail Schema for Retrieve Endpoint
|
|
68
|
+
|
|
69
|
+
Use `schema_detail` when you want the retrieve endpoint to return more fields than the list endpoint:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
class ArticleSerializer(serializers.Serializer):
|
|
73
|
+
class Meta:
|
|
74
|
+
model = models.Article
|
|
75
|
+
schema_out = serializers.SchemaModelConfig(
|
|
76
|
+
# List view: minimal fields for performance
|
|
77
|
+
fields=["id", "title", "summary"]
|
|
78
|
+
)
|
|
79
|
+
schema_detail = serializers.SchemaModelConfig(
|
|
80
|
+
# Detail view: all fields including expensive relations
|
|
81
|
+
fields=["id", "title", "summary", "content", "author", "tags"],
|
|
82
|
+
customs=[("reading_time", int, lambda obj: len(obj.content.split()) // 200)]
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
When used with `APIViewSet`:
|
|
87
|
+
- **List endpoint** (`GET /articles/`) uses `schema_out`
|
|
88
|
+
- **Retrieve endpoint** (`GET /articles/{pk}`) uses `schema_detail` (falls back to `schema_out` if not defined)
|
|
89
|
+
|
|
65
90
|
## Example: simple FK
|
|
66
91
|
|
|
67
92
|
```python
|
|
@@ -450,12 +475,19 @@ class ArticleSerializer(serializers.Serializer):
|
|
|
450
475
|
schema_out = serializers.SchemaModelConfig(
|
|
451
476
|
fields=["id", "title", "content", "author", "category"]
|
|
452
477
|
)
|
|
478
|
+
schema_detail = serializers.SchemaModelConfig(
|
|
479
|
+
fields=["id", "title", "content", "author", "category", "tags", "comments"]
|
|
480
|
+
)
|
|
453
481
|
|
|
454
482
|
class QuerySet:
|
|
455
483
|
read = ModelQuerySetSchema(
|
|
456
484
|
select_related=["author", "category"],
|
|
457
485
|
prefetch_related=["tags"],
|
|
458
486
|
)
|
|
487
|
+
detail = ModelQuerySetSchema(
|
|
488
|
+
select_related=["author", "category", "author__profile"],
|
|
489
|
+
prefetch_related=["tags", "comments", "comments__author"],
|
|
490
|
+
)
|
|
459
491
|
queryset_request = ModelQuerySetSchema(
|
|
460
492
|
select_related=[],
|
|
461
493
|
prefetch_related=["comments"],
|
|
@@ -469,7 +501,12 @@ class ArticleSerializer(serializers.Serializer):
|
|
|
469
501
|
]
|
|
470
502
|
```
|
|
471
503
|
|
|
472
|
-
The QuerySet configuration is used by ModelUtil to automatically optimize database queries
|
|
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`
|
|
473
510
|
|
|
474
511
|
## Complete Example
|
|
475
512
|
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
## Generated CRUD Endpoints
|
|
6
6
|
|
|
7
|
-
| Method | Path | Summary | Response
|
|
8
|
-
| ------ | --------------- | -------------- |
|
|
9
|
-
| POST | `/{base}/` | Create Model | `201 schema_out`
|
|
10
|
-
| GET | `/{base}/` | List Models | `200 List[schema_out]` (paginated)
|
|
11
|
-
| GET | `/{base}/{pk}` | Retrieve Model | `200 schema_out`
|
|
12
|
-
| PATCH | `/{base}/{pk}/` | Update Model | `200 schema_out`
|
|
13
|
-
| DELETE | `/{base}/{pk}/` | Delete Model | `204 No Content`
|
|
7
|
+
| Method | Path | Summary | Response |
|
|
8
|
+
| ------ | --------------- | -------------- | --------------------------------------------- |
|
|
9
|
+
| POST | `/{base}/` | Create Model | `201 schema_out` |
|
|
10
|
+
| GET | `/{base}/` | List Models | `200 List[schema_out]` (paginated) |
|
|
11
|
+
| GET | `/{base}/{pk}` | Retrieve Model | `200 schema_detail` (or `schema_out` if none) |
|
|
12
|
+
| PATCH | `/{base}/{pk}/` | Update Model | `200 schema_out` |
|
|
13
|
+
| DELETE | `/{base}/{pk}/` | Delete Model | `204 No Content` |
|
|
14
14
|
|
|
15
15
|
Notes:
|
|
16
16
|
|
|
@@ -100,7 +100,8 @@ class ArticleViewSet(APIViewSet):
|
|
|
100
100
|
| `api` | `NinjaAPI` | — | API instance (required) |
|
|
101
101
|
| `serializer_class` | `Serializer \| None` | `None` | Serializer class for plain models (alternative to ModelSerializer) |
|
|
102
102
|
| `schema_in` | `Schema \| None` | `None` (auto) | Create input schema override |
|
|
103
|
-
| `schema_out` | `Schema \| None` | `None` (auto) |
|
|
103
|
+
| `schema_out` | `Schema \| None` | `None` (auto) | List/output schema override |
|
|
104
|
+
| `schema_detail` | `Schema \| None` | `None` (auto) | Retrieve/detail schema override (falls back to `schema_out`) |
|
|
104
105
|
| `schema_update` | `Schema \| None` | `None` (auto) | Update input schema override |
|
|
105
106
|
| `pagination_class` | `type[AsyncPaginationBase]` | `PageNumberPagination` | Pagination strategy |
|
|
106
107
|
| `query_params` | `dict[str, tuple[type, ...]]` | `{}` | List endpoint filters definition |
|
|
@@ -150,6 +151,7 @@ The transaction behavior is applied by default. Custom decorators can be added v
|
|
|
150
151
|
If `model` is a subclass of `ModelSerializerMeta`:
|
|
151
152
|
|
|
152
153
|
- `schema_out` is generated from `ReadSerializer`
|
|
154
|
+
- `schema_detail` is generated from `DetailSerializer` (optional, falls back to `schema_out`)
|
|
153
155
|
- `schema_in` from `CreateSerializer`
|
|
154
156
|
- `schema_update` from `UpdateSerializer`
|
|
155
157
|
|
|
@@ -173,7 +175,42 @@ class ArticleViewSet(APIViewSet):
|
|
|
173
175
|
serializer_class = ArticleSerializer
|
|
174
176
|
```
|
|
175
177
|
|
|
176
|
-
Otherwise provide schemas manually via `schema_in`, `schema_out`, and `schema_update` attributes.
|
|
178
|
+
Otherwise provide schemas manually via `schema_in`, `schema_out`, `schema_detail`, and `schema_update` attributes.
|
|
179
|
+
|
|
180
|
+
### Detail Schema for Retrieve Endpoint
|
|
181
|
+
|
|
182
|
+
Use `schema_detail` (or `DetailSerializer` on ModelSerializer) when you want the retrieve endpoint to return more fields than the list endpoint. This is useful for:
|
|
183
|
+
|
|
184
|
+
- **Performance optimization**: List endpoints return minimal fields, retrieve endpoints include expensive relations
|
|
185
|
+
- **API design**: Clients get a summary in lists and full details on individual requests
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from ninja_aio.models import ModelSerializer
|
|
189
|
+
from django.db import models
|
|
190
|
+
|
|
191
|
+
class Article(ModelSerializer):
|
|
192
|
+
title = models.CharField(max_length=200)
|
|
193
|
+
summary = models.TextField()
|
|
194
|
+
content = models.TextField()
|
|
195
|
+
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
196
|
+
tags = models.ManyToManyField(Tag)
|
|
197
|
+
|
|
198
|
+
class ReadSerializer:
|
|
199
|
+
# List view: minimal fields
|
|
200
|
+
fields = ["id", "title", "summary"]
|
|
201
|
+
|
|
202
|
+
class DetailSerializer:
|
|
203
|
+
# Detail view: all fields
|
|
204
|
+
fields = ["id", "title", "summary", "content", "author", "tags"]
|
|
205
|
+
|
|
206
|
+
@api.viewset(model=Article)
|
|
207
|
+
class ArticleViewSet(APIViewSet):
|
|
208
|
+
pass # Schemas auto-generated from model
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Endpoints behavior:
|
|
212
|
+
- `GET /articles/` returns `[{"id": 1, "title": "...", "summary": "..."}, ...]`
|
|
213
|
+
- `GET /articles/1` returns `{"id": 1, "title": "...", "summary": "...", "content": "...", "author": {...}, "tags": [...]}`
|
|
177
214
|
|
|
178
215
|
## List Filtering
|
|
179
216
|
|
|
@@ -254,6 +291,7 @@ Relations are declared via `M2MRelationSchema` objects (not tuples). Each schema
|
|
|
254
291
|
- `get`: enable GET listing (bool)
|
|
255
292
|
- `filters`: dict of `{param_name: (type, default)}` for relation-level filtering
|
|
256
293
|
- `related_schema`: optional pre-built schema for the related model (auto-generated if the `model` is a `ModelSerializer`)
|
|
294
|
+
- `serializer_class`: optional `Serializer` class for plain Django models. When provided, `related_schema` is auto-generated from the serializer. Cannot be used when `model` is a `ModelSerializer`.
|
|
257
295
|
- `append_slash`: bool to control trailing slash for the GET relation endpoint path. Defaults to `False` (no trailing slash) for backward compatibility. When `True`, the GET path ends with a trailing slash.
|
|
258
296
|
|
|
259
297
|
If `path` is empty it falls back to the related model verbose name (lowercase plural).
|
|
@@ -285,21 +323,43 @@ async def tags_query_params_handler(self, queryset, filters_dict):
|
|
|
285
323
|
|
|
286
324
|
Warning: Model support
|
|
287
325
|
|
|
288
|
-
- You can supply a standard Django `Model` (not a `ModelSerializer`) in `M2MRelationSchema.model`. When doing so you must provide `related_schema` manually
|
|
326
|
+
- You can supply a standard Django `Model` (not a `ModelSerializer`) in `M2MRelationSchema.model`. When doing so you must provide either `related_schema` manually or `serializer_class`:
|
|
289
327
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
=== "With related_schema"
|
|
329
|
+
```python
|
|
330
|
+
M2MRelationSchema(
|
|
331
|
+
model=Tag, # plain django.db.models.Model
|
|
332
|
+
related_name="tags",
|
|
333
|
+
related_schema=TagOut, # a Pydantic/Ninja Schema you define
|
|
334
|
+
add=True,
|
|
335
|
+
remove=True,
|
|
336
|
+
get=True,
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
=== "With serializer_class"
|
|
341
|
+
```python
|
|
342
|
+
from ninja_aio.models import serializers
|
|
343
|
+
|
|
344
|
+
class TagSerializer(serializers.Serializer):
|
|
345
|
+
class Meta:
|
|
346
|
+
model = Tag
|
|
347
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
348
|
+
|
|
349
|
+
M2MRelationSchema(
|
|
350
|
+
model=Tag, # plain django.db.models.Model
|
|
351
|
+
related_name="tags",
|
|
352
|
+
serializer_class=TagSerializer, # auto-generates related_schema
|
|
353
|
+
add=True,
|
|
354
|
+
remove=True,
|
|
355
|
+
get=True,
|
|
356
|
+
)
|
|
357
|
+
```
|
|
300
358
|
|
|
301
359
|
For `ModelSerializer` models, `related_schema` can be inferred automatically (via internal helpers).
|
|
302
360
|
|
|
361
|
+
Note: You cannot use `serializer_class` when `model` is already a `ModelSerializer` - this will raise a `ValueError`.
|
|
362
|
+
|
|
303
363
|
Example with filters:
|
|
304
364
|
|
|
305
365
|
```python
|
|
@@ -472,7 +472,7 @@ class ManyToManyAPI:
|
|
|
472
472
|
model = relation.model
|
|
473
473
|
related_name = relation.related_name
|
|
474
474
|
m2m_auth = relation.auth or self.default_auth
|
|
475
|
-
rel_util = ModelUtil(model)
|
|
475
|
+
rel_util = ModelUtil(model, serializer_class=relation.serializer_class)
|
|
476
476
|
rel_path = relation.path or rel_util.verbose_name_path_resolver()
|
|
477
477
|
related_schema = relation.related_schema
|
|
478
478
|
m2m_add, m2m_remove, m2m_get = relation.add, relation.remove, relation.get
|
|
@@ -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."""
|