django-ninja-aio-crud 2.16.0__tar.gz → 2.16.1__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.16.0 → django_ninja_aio_crud-2.16.1}/.github/workflows/docs.yml +2 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/views/api_view_set.md +40 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/views/decorators.md +17 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/quick_start.md +9 -7
- django_ninja_aio_crud-2.16.1/docs/getting_started/quick_start_serializer.md +220 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/mkdocs.yml +2 -1
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/exceptions.py +1 -1
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/serializers.py +11 -2
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_serializers.py +155 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/.gitignore +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/LICENSE +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/README.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/models/serializers.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/main.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/filters.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_app/models.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/views/test_views.py +0 -0
- {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/views/test_viewset.py +0 -0
|
@@ -28,6 +28,7 @@ on:
|
|
|
28
28
|
- "2.13"
|
|
29
29
|
- "2.14"
|
|
30
30
|
- "2.15"
|
|
31
|
+
- "2.16"
|
|
31
32
|
make_latest:
|
|
32
33
|
description: 'Set as "latest" and default?'
|
|
33
34
|
type: boolean
|
|
@@ -59,6 +60,7 @@ on:
|
|
|
59
60
|
- "2.13"
|
|
60
61
|
- "2.14"
|
|
61
62
|
- "2.15"
|
|
63
|
+
- "2.16"
|
|
62
64
|
delete_confirm:
|
|
63
65
|
description: 'Confirm deletion of the selected version'
|
|
64
66
|
type: boolean
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/views/api_view_set.md
RENAMED
|
@@ -294,6 +294,8 @@ Relations are declared via `M2MRelationSchema` objects (not tuples). Each schema
|
|
|
294
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`.
|
|
295
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.
|
|
296
296
|
- `verbose_name_plural`: optional human-readable plural name for the related model, used in endpoint summaries and descriptions. When not provided, defaults to the related model's `_meta.verbose_name_plural`.
|
|
297
|
+
- `get_decorators`: optional list of decorators to apply to the GET (list related objects) endpoint. Decorators are applied via `decorate_view()` alongside built-in decorators like `unique_view` and `paginate`.
|
|
298
|
+
- `post_decorators`: optional list of decorators to apply to the POST (add/remove) endpoint. Decorators are applied via `decorate_view()` alongside the built-in `unique_view` decorator.
|
|
297
299
|
|
|
298
300
|
If `path` is empty it falls back to the related model verbose name (lowercase plural).
|
|
299
301
|
If `filters` is provided, a per-relation filters schema is auto-generated and exposed on the GET relation endpoint:
|
|
@@ -489,6 +491,44 @@ M2MRelationSchema(
|
|
|
489
491
|
)
|
|
490
492
|
```
|
|
491
493
|
|
|
494
|
+
Example with custom decorators:
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
from functools import wraps
|
|
498
|
+
|
|
499
|
+
def cache_response(timeout=60):
|
|
500
|
+
"""Cache decorator for GET endpoints."""
|
|
501
|
+
def decorator(func):
|
|
502
|
+
@wraps(func)
|
|
503
|
+
async def wrapper(*args, **kwargs):
|
|
504
|
+
# caching logic here
|
|
505
|
+
return await func(*args, **kwargs)
|
|
506
|
+
return wrapper
|
|
507
|
+
return decorator
|
|
508
|
+
|
|
509
|
+
def rate_limit(max_calls=100):
|
|
510
|
+
"""Rate limiting decorator for POST endpoints."""
|
|
511
|
+
def decorator(func):
|
|
512
|
+
@wraps(func)
|
|
513
|
+
async def wrapper(*args, **kwargs):
|
|
514
|
+
# rate limiting logic here
|
|
515
|
+
return await func(*args, **kwargs)
|
|
516
|
+
return wrapper
|
|
517
|
+
return decorator
|
|
518
|
+
|
|
519
|
+
M2MRelationSchema(
|
|
520
|
+
model=Tag,
|
|
521
|
+
related_name="tags",
|
|
522
|
+
get_decorators=[cache_response(timeout=300)], # Cache GET for 5 minutes
|
|
523
|
+
post_decorators=[rate_limit(max_calls=50)], # Rate limit POST operations
|
|
524
|
+
add=True,
|
|
525
|
+
remove=True,
|
|
526
|
+
get=True,
|
|
527
|
+
)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Note: Decorators are applied in addition to built-in decorators (`unique_view`, `paginate`). The order follows standard Python decorator stacking: decorators listed first are applied outermost.
|
|
531
|
+
|
|
492
532
|
## Custom Views
|
|
493
533
|
|
|
494
534
|
Preferred (decorators): see the section above.
|
|
@@ -63,6 +63,23 @@ class MyViewSet(APIViewSet):
|
|
|
63
63
|
|
|
64
64
|
These are applied in combination with built-ins (e.g., unique_view, paginate) using decorate_view in the implementation.
|
|
65
65
|
|
|
66
|
+
## M2MRelationSchema decorators
|
|
67
|
+
|
|
68
|
+
Apply custom decorators to Many-to-Many relation endpoints via `get_decorators` and `post_decorators`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from ninja_aio.schemas import M2MRelationSchema
|
|
72
|
+
|
|
73
|
+
M2MRelationSchema(
|
|
74
|
+
model=Tag,
|
|
75
|
+
related_name="tags",
|
|
76
|
+
get_decorators=[cache_decorator, log_decorator], # Applied to GET (list related)
|
|
77
|
+
post_decorators=[rate_limit_decorator], # Applied to POST (add/remove)
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
These decorators are applied alongside built-in decorators (`unique_view`, `paginate`) using `decorate_view`. See [APIViewSet M2M Relations](api_view_set.md#many-to-many-relations) for more details.
|
|
82
|
+
|
|
66
83
|
## ApiMethodFactory.decorators
|
|
67
84
|
|
|
68
85
|
Example: use api_get within a ViewSet with extra decorators:
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/quick_start.md
RENAMED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
## 🚀 Quick Start
|
|
1
|
+
## 🚀 Quick Start (ModelSerializer)
|
|
2
|
+
|
|
3
|
+
This guide shows how to create a CRUD API using `ModelSerializer`, which combines your Django model and serialization configuration in a single class.
|
|
4
|
+
|
|
5
|
+
!!! tip "Alternative Approach"
|
|
6
|
+
If you prefer to keep your models unchanged and define serialization separately, see [Quick Start (Serializer)](quick_start_serializer.md).
|
|
2
7
|
|
|
3
8
|
### 1. Create Your Model
|
|
4
9
|
|
|
5
|
-
Define your model using `ModelSerializer
|
|
10
|
+
Define your model using `ModelSerializer` with embedded serializer configuration:
|
|
6
11
|
|
|
7
12
|
```python
|
|
8
13
|
# models.py
|
|
@@ -44,12 +49,9 @@ from .models import Article
|
|
|
44
49
|
api = NinjaAIO(title="My Blog API", version="1.0.0")
|
|
45
50
|
|
|
46
51
|
|
|
52
|
+
@api.viewset(model=Article)
|
|
47
53
|
class ArticleViewSet(APIViewSet):
|
|
48
|
-
|
|
49
|
-
api = api
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
ArticleViewSet().add_views_to_route()
|
|
54
|
+
pass
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
### 3. Configure URLs
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
## 🚀 Quick Start (Serializer)
|
|
2
|
+
|
|
3
|
+
This guide shows how to create a CRUD API using the `Serializer` class with plain Django models. This approach keeps your models unchanged and defines API configuration separately.
|
|
4
|
+
|
|
5
|
+
!!! tip "Alternative Approach"
|
|
6
|
+
If you prefer an all-in-one approach with embedded serialization, see [Quick Start (ModelSerializer)](quick_start.md).
|
|
7
|
+
|
|
8
|
+
### When to Use Serializer
|
|
9
|
+
|
|
10
|
+
Choose the `Serializer` approach when:
|
|
11
|
+
|
|
12
|
+
- You have existing Django models you don't want to modify
|
|
13
|
+
- You want to keep models and API concerns separated
|
|
14
|
+
- You're adding API functionality to an existing project
|
|
15
|
+
- Multiple teams work on models vs. API layers
|
|
16
|
+
|
|
17
|
+
### 1. Create Your Model
|
|
18
|
+
|
|
19
|
+
Use a standard Django model:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# models.py
|
|
23
|
+
from django.db import models
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Article(models.Model):
|
|
27
|
+
title = models.CharField(max_length=200)
|
|
28
|
+
content = models.TextField()
|
|
29
|
+
is_published = models.BooleanField(default=False)
|
|
30
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
31
|
+
|
|
32
|
+
class Meta:
|
|
33
|
+
ordering = ["-created_at"]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Create Your Serializer
|
|
37
|
+
|
|
38
|
+
Define a `Serializer` class with API configuration in a nested `Meta` class:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# serializers.py
|
|
42
|
+
from ninja_aio.models import serializers
|
|
43
|
+
from .models import Article
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ArticleSerializer(serializers.Serializer):
|
|
47
|
+
class Meta:
|
|
48
|
+
model = Article
|
|
49
|
+
schema_in = serializers.SchemaModelConfig(
|
|
50
|
+
fields=["title", "content"],
|
|
51
|
+
optionals=[("is_published", bool)],
|
|
52
|
+
)
|
|
53
|
+
schema_out = serializers.SchemaModelConfig(
|
|
54
|
+
fields=["id", "title", "content", "is_published", "created_at"]
|
|
55
|
+
)
|
|
56
|
+
schema_update = serializers.SchemaModelConfig(
|
|
57
|
+
optionals=[
|
|
58
|
+
("title", str),
|
|
59
|
+
("content", str),
|
|
60
|
+
("is_published", bool),
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Create Your ViewSet
|
|
66
|
+
|
|
67
|
+
Define your API views using `APIViewSet` with `serializer_class`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# views.py
|
|
71
|
+
from ninja_aio import NinjaAIO
|
|
72
|
+
from ninja_aio.views import APIViewSet
|
|
73
|
+
from .models import Article
|
|
74
|
+
from .serializers import ArticleSerializer
|
|
75
|
+
|
|
76
|
+
api = NinjaAIO(title="My Blog API", version="1.0.0")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@api.viewset(model=Article)
|
|
80
|
+
class ArticleViewSet(APIViewSet):
|
|
81
|
+
serializer_class = ArticleSerializer
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Configure URLs
|
|
85
|
+
|
|
86
|
+
Add the API to your URL configuration:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# urls.py
|
|
90
|
+
from django.urls import path
|
|
91
|
+
from .views import api
|
|
92
|
+
|
|
93
|
+
urlpatterns = [
|
|
94
|
+
path("api/", api.urls),
|
|
95
|
+
]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 5. Run Your Server
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python manage.py runserver
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Visit **[http://localhost:8000/api/docs](http://localhost:8000/api/docs)** to see your auto-generated API documentation!
|
|
105
|
+
|
|
106
|
+
## Adding Relationships
|
|
107
|
+
|
|
108
|
+
The `Serializer` approach supports nested serialization for related models:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# models.py
|
|
112
|
+
from django.db import models
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Author(models.Model):
|
|
116
|
+
name = models.CharField(max_length=200)
|
|
117
|
+
email = models.EmailField()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Article(models.Model):
|
|
121
|
+
title = models.CharField(max_length=200)
|
|
122
|
+
content = models.TextField()
|
|
123
|
+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="articles")
|
|
124
|
+
is_published = models.BooleanField(default=False)
|
|
125
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# serializers.py
|
|
130
|
+
from ninja_aio.models import serializers
|
|
131
|
+
from .models import Author, Article
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AuthorSerializer(serializers.Serializer):
|
|
135
|
+
class Meta:
|
|
136
|
+
model = Author
|
|
137
|
+
schema_out = serializers.SchemaModelConfig(
|
|
138
|
+
fields=["id", "name", "email"]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ArticleSerializer(serializers.Serializer):
|
|
143
|
+
class Meta:
|
|
144
|
+
model = Article
|
|
145
|
+
schema_in = serializers.SchemaModelConfig(
|
|
146
|
+
fields=["title", "content", "author"],
|
|
147
|
+
)
|
|
148
|
+
schema_out = serializers.SchemaModelConfig(
|
|
149
|
+
fields=["id", "title", "content", "author", "is_published", "created_at"]
|
|
150
|
+
)
|
|
151
|
+
schema_update = serializers.SchemaModelConfig(
|
|
152
|
+
optionals=[
|
|
153
|
+
("title", str),
|
|
154
|
+
("content", str),
|
|
155
|
+
("is_published", bool),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
# Nested serialization for the author field
|
|
159
|
+
relations_serializers = {
|
|
160
|
+
"author": AuthorSerializer,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class QuerySet:
|
|
164
|
+
# Optimize queries with select_related
|
|
165
|
+
read = serializers.ModelQuerySetSchema(
|
|
166
|
+
select_related=["author"]
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Adding Lifecycle Hooks
|
|
171
|
+
|
|
172
|
+
Add custom logic to CRUD operations using hooks:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# serializers.py
|
|
176
|
+
from ninja_aio.models import serializers
|
|
177
|
+
from .models import Article
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ArticleSerializer(serializers.Serializer):
|
|
181
|
+
class Meta:
|
|
182
|
+
model = Article
|
|
183
|
+
schema_in = serializers.SchemaModelConfig(
|
|
184
|
+
fields=["title", "content"],
|
|
185
|
+
customs=[("notify_subscribers", bool, False)], # Custom field
|
|
186
|
+
)
|
|
187
|
+
schema_out = serializers.SchemaModelConfig(
|
|
188
|
+
fields=["id", "title", "content", "is_published", "created_at"]
|
|
189
|
+
)
|
|
190
|
+
schema_update = serializers.SchemaModelConfig(
|
|
191
|
+
optionals=[("title", str), ("content", str), ("is_published", bool)]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def custom_actions(self, payload, instance):
|
|
195
|
+
"""Execute after field assignment, before save."""
|
|
196
|
+
if payload.get("notify_subscribers"):
|
|
197
|
+
await send_notification(instance.title)
|
|
198
|
+
|
|
199
|
+
async def post_create(self, instance):
|
|
200
|
+
"""Execute after instance creation."""
|
|
201
|
+
await AuditLog.objects.acreate(
|
|
202
|
+
action="article_created",
|
|
203
|
+
article_id=instance.id
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def before_save(self, instance):
|
|
207
|
+
"""Execute before any save (sync hook)."""
|
|
208
|
+
instance.slug = slugify(instance.title)
|
|
209
|
+
|
|
210
|
+
def on_delete(self, instance):
|
|
211
|
+
"""Execute after deletion (sync hook)."""
|
|
212
|
+
logger.info(f"Article {instance.id} deleted")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Next Steps
|
|
216
|
+
|
|
217
|
+
- Learn more about [Serializer configuration](../api/models/serializers.md)
|
|
218
|
+
- Explore [APIViewSet features](../api/views/api_view_set.md)
|
|
219
|
+
- Add [Authentication](../api/authentication.md) to your API
|
|
220
|
+
- Configure [Pagination](../api/pagination.md)
|
|
@@ -9,7 +9,8 @@ nav:
|
|
|
9
9
|
- Home: index.md
|
|
10
10
|
- Getting Started:
|
|
11
11
|
- Installation: getting_started/installation.md
|
|
12
|
-
- Quick Start: getting_started/quick_start.md
|
|
12
|
+
- Quick Start (ModelSerializer): getting_started/quick_start.md
|
|
13
|
+
- Quick Start (Serializer): getting_started/quick_start_serializer.md
|
|
13
14
|
- Tutorial:
|
|
14
15
|
- 'Step 1: Define Your Model': tutorial/model.md
|
|
15
16
|
- 'Step 2: Create CRUD Views': tutorial/crud.md
|
|
@@ -78,7 +78,7 @@ def _default_error(
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
def _pydantic_validation_error(
|
|
81
|
-
request: HttpRequest, exc:
|
|
81
|
+
request: HttpRequest, exc: PydanticValidationError, api: type[NinjaAPI]
|
|
82
82
|
) -> HttpResponse:
|
|
83
83
|
"""Translate a pydantic ValidationError into a normalized API error response."""
|
|
84
84
|
error = PydanticValidationError(exc.errors(include_input=False))
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/serializers.py
RENAMED
|
@@ -669,9 +669,18 @@ class BaseSerializer:
|
|
|
669
669
|
customs = cls.get_custom_fields(s_type) + optionals
|
|
670
670
|
excludes = cls.get_excluded_fields(s_type)
|
|
671
671
|
|
|
672
|
-
# If no explicit fields
|
|
672
|
+
# If no explicit fields and no excludes specified
|
|
673
673
|
if not fields and not excludes:
|
|
674
|
-
|
|
674
|
+
if optionals:
|
|
675
|
+
# Use optional field names as the fields to include
|
|
676
|
+
fields = [f[0] for f in optionals]
|
|
677
|
+
elif customs:
|
|
678
|
+
# Only customs defined - exclude all model fields to prevent auto-inclusion
|
|
679
|
+
excludes = [
|
|
680
|
+
f.name
|
|
681
|
+
for f in model._meta.get_fields()
|
|
682
|
+
if getattr(f, "concrete", False)
|
|
683
|
+
]
|
|
675
684
|
|
|
676
685
|
# Only create schema if we have something to include
|
|
677
686
|
if not any([fields, customs, excludes]):
|
|
@@ -1227,6 +1227,161 @@ class RelationsAsIdStringPKModelSerializerTestCase(TestCase):
|
|
|
1227
1227
|
)
|
|
1228
1228
|
|
|
1229
1229
|
|
|
1230
|
+
@tag("serializers", "customs_only")
|
|
1231
|
+
class CustomsOnlySchemaTestCase(TestCase):
|
|
1232
|
+
"""Test cases for schemas with only customs/optionals defined (no fields/excludes)."""
|
|
1233
|
+
|
|
1234
|
+
def setUp(self):
|
|
1235
|
+
warnings.simplefilter("ignore", UserWarning)
|
|
1236
|
+
|
|
1237
|
+
def test_serializer_create_schema_with_only_customs(self):
|
|
1238
|
+
"""Test that create schema with only customs does NOT include model fields."""
|
|
1239
|
+
|
|
1240
|
+
class CustomsOnlyCreateSerializer(serializers.Serializer):
|
|
1241
|
+
class Meta:
|
|
1242
|
+
model = TestModelForeignKey
|
|
1243
|
+
schema_in = serializers.SchemaModelConfig(
|
|
1244
|
+
customs=[("custom_input", str, ...)]
|
|
1245
|
+
)
|
|
1246
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1247
|
+
|
|
1248
|
+
schema = CustomsOnlyCreateSerializer.generate_create_s()
|
|
1249
|
+
self.assertIsNotNone(schema)
|
|
1250
|
+
# Should only have the custom field, not model fields
|
|
1251
|
+
self.assertIn("custom_input", schema.model_fields)
|
|
1252
|
+
# Should NOT have model fields auto-included
|
|
1253
|
+
self.assertNotIn("name", schema.model_fields)
|
|
1254
|
+
self.assertNotIn("description", schema.model_fields)
|
|
1255
|
+
self.assertNotIn("test_model", schema.model_fields)
|
|
1256
|
+
self.assertNotIn("id", schema.model_fields)
|
|
1257
|
+
|
|
1258
|
+
def test_serializer_update_schema_with_only_customs(self):
|
|
1259
|
+
"""Test that update schema with only customs does NOT include model fields."""
|
|
1260
|
+
|
|
1261
|
+
class CustomsOnlyUpdateSerializer(serializers.Serializer):
|
|
1262
|
+
class Meta:
|
|
1263
|
+
model = TestModelForeignKey
|
|
1264
|
+
schema_update = serializers.SchemaModelConfig(
|
|
1265
|
+
customs=[("custom_patch_field", str, None)]
|
|
1266
|
+
)
|
|
1267
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1268
|
+
|
|
1269
|
+
schema = CustomsOnlyUpdateSerializer.generate_update_s()
|
|
1270
|
+
self.assertIsNotNone(schema)
|
|
1271
|
+
# Should only have the custom field
|
|
1272
|
+
self.assertIn("custom_patch_field", schema.model_fields)
|
|
1273
|
+
# Should NOT have model fields auto-included
|
|
1274
|
+
self.assertNotIn("name", schema.model_fields)
|
|
1275
|
+
self.assertNotIn("description", schema.model_fields)
|
|
1276
|
+
self.assertNotIn("test_model", schema.model_fields)
|
|
1277
|
+
self.assertNotIn("id", schema.model_fields)
|
|
1278
|
+
|
|
1279
|
+
def test_serializer_create_schema_with_customs_and_optionals(self):
|
|
1280
|
+
"""Test create schema with customs + optionals includes only those fields."""
|
|
1281
|
+
|
|
1282
|
+
class CustomsAndOptionalsSerializer(serializers.Serializer):
|
|
1283
|
+
class Meta:
|
|
1284
|
+
model = TestModelForeignKey
|
|
1285
|
+
schema_in = serializers.SchemaModelConfig(
|
|
1286
|
+
customs=[("custom_field", str, ...)],
|
|
1287
|
+
optionals=[("name", str)], # name is a model field made optional
|
|
1288
|
+
)
|
|
1289
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1290
|
+
|
|
1291
|
+
schema = CustomsAndOptionalsSerializer.generate_create_s()
|
|
1292
|
+
self.assertIsNotNone(schema)
|
|
1293
|
+
# Should have custom field
|
|
1294
|
+
self.assertIn("custom_field", schema.model_fields)
|
|
1295
|
+
# Should have the optional model field
|
|
1296
|
+
self.assertIn("name", schema.model_fields)
|
|
1297
|
+
# Should NOT have other model fields
|
|
1298
|
+
self.assertNotIn("description", schema.model_fields)
|
|
1299
|
+
self.assertNotIn("test_model", schema.model_fields)
|
|
1300
|
+
self.assertNotIn("id", schema.model_fields)
|
|
1301
|
+
|
|
1302
|
+
def test_serializer_with_fields_still_works(self):
|
|
1303
|
+
"""Test that defining fields still works as expected."""
|
|
1304
|
+
|
|
1305
|
+
class FieldsDefinedSerializer(serializers.Serializer):
|
|
1306
|
+
class Meta:
|
|
1307
|
+
model = TestModelForeignKey
|
|
1308
|
+
schema_in = serializers.SchemaModelConfig(
|
|
1309
|
+
fields=["name", "description"],
|
|
1310
|
+
customs=[("extra", int, 0)],
|
|
1311
|
+
)
|
|
1312
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1313
|
+
|
|
1314
|
+
schema = FieldsDefinedSerializer.generate_create_s()
|
|
1315
|
+
self.assertIsNotNone(schema)
|
|
1316
|
+
# Should have specified fields
|
|
1317
|
+
self.assertIn("name", schema.model_fields)
|
|
1318
|
+
self.assertIn("description", schema.model_fields)
|
|
1319
|
+
# Should have custom field
|
|
1320
|
+
self.assertIn("extra", schema.model_fields)
|
|
1321
|
+
# Should NOT have other model fields
|
|
1322
|
+
self.assertNotIn("test_model", schema.model_fields)
|
|
1323
|
+
self.assertNotIn("id", schema.model_fields)
|
|
1324
|
+
|
|
1325
|
+
def test_serializer_with_only_excludes_and_customs(self):
|
|
1326
|
+
"""Test schema with excludes and customs but no fields only has customs."""
|
|
1327
|
+
|
|
1328
|
+
class ExcludesOnlySerializer(serializers.Serializer):
|
|
1329
|
+
class Meta:
|
|
1330
|
+
model = TestModelForeignKey
|
|
1331
|
+
schema_in = serializers.SchemaModelConfig(
|
|
1332
|
+
exclude=["id", "test_model"],
|
|
1333
|
+
customs=[("extra", int, 0)],
|
|
1334
|
+
)
|
|
1335
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1336
|
+
|
|
1337
|
+
schema = ExcludesOnlySerializer.generate_create_s()
|
|
1338
|
+
self.assertIsNotNone(schema)
|
|
1339
|
+
# With excludes but no fields, only customs are included
|
|
1340
|
+
# because fields=[] is passed to create_schema (no model fields)
|
|
1341
|
+
self.assertIn("extra", schema.model_fields)
|
|
1342
|
+
# This is expected behavior: without explicit fields, no model fields included
|
|
1343
|
+
self.assertEqual(len(schema.model_fields), 1)
|
|
1344
|
+
|
|
1345
|
+
def test_serializer_empty_schema_returns_none(self):
|
|
1346
|
+
"""Test that schema with nothing defined returns None."""
|
|
1347
|
+
|
|
1348
|
+
class EmptySchemaSerializer(serializers.Serializer):
|
|
1349
|
+
class Meta:
|
|
1350
|
+
model = TestModelForeignKey
|
|
1351
|
+
schema_in = serializers.SchemaModelConfig()
|
|
1352
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1353
|
+
|
|
1354
|
+
schema = EmptySchemaSerializer.generate_create_s()
|
|
1355
|
+
self.assertIsNone(schema)
|
|
1356
|
+
|
|
1357
|
+
def test_serializer_multiple_customs_no_model_fields(self):
|
|
1358
|
+
"""Test schema with multiple customs but no model fields."""
|
|
1359
|
+
|
|
1360
|
+
class MultipleCustomsSerializer(serializers.Serializer):
|
|
1361
|
+
class Meta:
|
|
1362
|
+
model = TestModelForeignKey
|
|
1363
|
+
schema_in = serializers.SchemaModelConfig(
|
|
1364
|
+
customs=[
|
|
1365
|
+
("custom1", str, ...),
|
|
1366
|
+
("custom2", int, 0),
|
|
1367
|
+
("custom3", bool, False),
|
|
1368
|
+
]
|
|
1369
|
+
)
|
|
1370
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
|
|
1371
|
+
|
|
1372
|
+
schema = MultipleCustomsSerializer.generate_create_s()
|
|
1373
|
+
self.assertIsNotNone(schema)
|
|
1374
|
+
# Should have all custom fields
|
|
1375
|
+
self.assertIn("custom1", schema.model_fields)
|
|
1376
|
+
self.assertIn("custom2", schema.model_fields)
|
|
1377
|
+
self.assertIn("custom3", schema.model_fields)
|
|
1378
|
+
# Should NOT have any model fields
|
|
1379
|
+
self.assertNotIn("name", schema.model_fields)
|
|
1380
|
+
self.assertNotIn("description", schema.model_fields)
|
|
1381
|
+
self.assertNotIn("test_model", schema.model_fields)
|
|
1382
|
+
self.assertNotIn("id", schema.model_fields)
|
|
1383
|
+
|
|
1384
|
+
|
|
1230
1385
|
@tag("serializers", "relations_as_id", "string_pk", "integration")
|
|
1231
1386
|
class RelationsAsIdStringPKIntegrationTestCase(TestCase):
|
|
1232
1387
|
"""Integration tests for relations_as_id with string primary keys and actual data serialization."""
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/.github/workflows/coverage.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/models/model_serializer.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/models/serializers.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/api/renderers/orjson_renderer.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/getting_started/installation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/docs/tutorial/authentication.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/__init__.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/ninja_aio/factory/operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/core/test_exceptions_api.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/core/test_renderer_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/helpers/test_many_to_many_api.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/models/test_model_util.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.1}/tests/models/test_models_extra.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|