django-ninja-aio-crud 2.15.1__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.15.1 → django_ninja_aio_crud-2.16.1}/.github/workflows/docs.yml +2 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/views/api_view_set.md +40 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/views/decorators.md +17 -0
- {django_ninja_aio_crud-2.15.1 → 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.15.1 → django_ninja_aio_crud-2.16.1}/mkdocs.yml +2 -1
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/exceptions.py +1 -1
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/helpers/api.py +11 -1
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/serializers.py +11 -2
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/helpers.py +8 -2
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/helpers/test_many_to_many_api.py +155 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_serializers.py +155 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/.gitignore +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/LICENSE +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/README.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/models/serializers.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/main.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/filters.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_app/models.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/views/test_views.py +0 -0
- {django_ninja_aio_crud-2.15.1 → 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.15.1 → 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.15.1 → 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))
|
|
@@ -359,6 +359,7 @@ class ManyToManyAPI:
|
|
|
359
359
|
filters_schema,
|
|
360
360
|
append_slash: bool,
|
|
361
361
|
verbose_name_plural: str,
|
|
362
|
+
decorators: list,
|
|
362
363
|
):
|
|
363
364
|
@self.router.get(
|
|
364
365
|
self._get_api_path(rel_path, append_slash=append_slash),
|
|
@@ -373,6 +374,7 @@ class ManyToManyAPI:
|
|
|
373
374
|
@decorate_view(
|
|
374
375
|
unique_view(f"get_{self.related_model_util.model_name}_{rel_path}"),
|
|
375
376
|
paginate(self.pagination_class),
|
|
377
|
+
*decorators,
|
|
376
378
|
)
|
|
377
379
|
async def get_related(
|
|
378
380
|
request: HttpRequest,
|
|
@@ -407,6 +409,7 @@ class ManyToManyAPI:
|
|
|
407
409
|
m2m_add: bool,
|
|
408
410
|
m2m_remove: bool,
|
|
409
411
|
verbose_name_plural: str,
|
|
412
|
+
decorators: list,
|
|
410
413
|
):
|
|
411
414
|
action, schema_in = self._resolve_action_schema(m2m_add, m2m_remove)
|
|
412
415
|
plural = verbose_name_plural
|
|
@@ -422,7 +425,10 @@ class ManyToManyAPI:
|
|
|
422
425
|
summary=summary,
|
|
423
426
|
description=summary,
|
|
424
427
|
)
|
|
425
|
-
@
|
|
428
|
+
@decorate_view(
|
|
429
|
+
unique_view(f"manage_{self.related_model_util.model_name}_{rel_path}"),
|
|
430
|
+
*decorators,
|
|
431
|
+
)
|
|
426
432
|
async def manage_related(
|
|
427
433
|
request: HttpRequest,
|
|
428
434
|
pk: Path[self.path_schema], # type: ignore
|
|
@@ -483,6 +489,8 @@ class ManyToManyAPI:
|
|
|
483
489
|
relation.verbose_name_plural
|
|
484
490
|
or rel_util.model._meta.verbose_name_plural.capitalize()
|
|
485
491
|
)
|
|
492
|
+
get_decorators = relation.get_decorators or []
|
|
493
|
+
post_decorators = relation.post_decorators or []
|
|
486
494
|
|
|
487
495
|
if m2m_get:
|
|
488
496
|
self._register_get_relation_view(
|
|
@@ -494,6 +502,7 @@ class ManyToManyAPI:
|
|
|
494
502
|
filters_schema=filters_schema,
|
|
495
503
|
append_slash=append_slash,
|
|
496
504
|
verbose_name_plural=verbose_name_plural,
|
|
505
|
+
decorators=get_decorators,
|
|
497
506
|
)
|
|
498
507
|
|
|
499
508
|
if m2m_add or m2m_remove:
|
|
@@ -505,6 +514,7 @@ class ManyToManyAPI:
|
|
|
505
514
|
m2m_add=m2m_add,
|
|
506
515
|
m2m_remove=m2m_remove,
|
|
507
516
|
verbose_name_plural=verbose_name_plural,
|
|
517
|
+
decorators=post_decorators,
|
|
508
518
|
)
|
|
509
519
|
|
|
510
520
|
def _add_views(self):
|
{django_ninja_aio_crud-2.15.1 → 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]):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Optional, Type
|
|
1
|
+
from typing import Callable, List, Optional, Type
|
|
2
2
|
|
|
3
3
|
from ninja import Schema
|
|
4
4
|
from ninja_aio.types import ModelSerializerMeta, SerializerMeta
|
|
@@ -46,7 +46,11 @@ class M2MRelationSchema(BaseModel):
|
|
|
46
46
|
Whether to append a trailing slash to the generated GET endpoint path. Defaults to False for backward compatibility.
|
|
47
47
|
verbose_name_plural (str | None):
|
|
48
48
|
Optional human-readable plural name for the related model, used in documentation and responses.
|
|
49
|
-
|
|
49
|
+
get_decorators (List | None):
|
|
50
|
+
Optional list of decorators to apply to the GET endpoint.
|
|
51
|
+
post_decorators (List | None):
|
|
52
|
+
Optional list of decorators to apply to the POST (add/remove) endpoints.
|
|
53
|
+
|
|
50
54
|
Validation:
|
|
51
55
|
- If `model` is not a ModelSerializerMeta, `related_schema` is required.
|
|
52
56
|
- When `model` is a ModelSerializerMeta and `related_schema` is not provided, it will be
|
|
@@ -72,6 +76,8 @@ class M2MRelationSchema(BaseModel):
|
|
|
72
76
|
serializer_class: Optional[SerializerMeta] = None
|
|
73
77
|
append_slash: bool = False
|
|
74
78
|
verbose_name_plural: Optional[str] = None
|
|
79
|
+
get_decorators: Optional[List[Callable]] = []
|
|
80
|
+
post_decorators: Optional[List[Callable]] = []
|
|
75
81
|
|
|
76
82
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
77
83
|
|
{django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/helpers/test_many_to_many_api.py
RENAMED
|
@@ -280,3 +280,158 @@ class M2MRelationSchemaValidationTestCase(TestCase):
|
|
|
280
280
|
serializer_class=TestModelReverseManyToManySerializer,
|
|
281
281
|
)
|
|
282
282
|
self.assertEqual(schema.related_schema, CustomSchema)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Decorator tracking for tests
|
|
286
|
+
_decorator_calls = {"get": 0, "post": 0}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def track_get_decorator(func):
|
|
290
|
+
"""Decorator that tracks GET endpoint calls."""
|
|
291
|
+
from functools import wraps
|
|
292
|
+
|
|
293
|
+
@wraps(func)
|
|
294
|
+
async def wrapper(*args, **kwargs):
|
|
295
|
+
_decorator_calls["get"] += 1
|
|
296
|
+
return await func(*args, **kwargs)
|
|
297
|
+
|
|
298
|
+
return wrapper
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def track_post_decorator(func):
|
|
302
|
+
"""Decorator that tracks POST endpoint calls."""
|
|
303
|
+
from functools import wraps
|
|
304
|
+
|
|
305
|
+
@wraps(func)
|
|
306
|
+
async def wrapper(*args, **kwargs):
|
|
307
|
+
_decorator_calls["post"] += 1
|
|
308
|
+
return await func(*args, **kwargs)
|
|
309
|
+
|
|
310
|
+
return wrapper
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class TestM2MWithDecoratorsViewSet(GenericAPIViewSet):
|
|
314
|
+
"""ViewSet using M2M relations with custom decorators."""
|
|
315
|
+
|
|
316
|
+
model = models.TestModelSerializerManyToMany
|
|
317
|
+
m2m_relations = [
|
|
318
|
+
M2MRelationSchema(
|
|
319
|
+
model=models.TestModelSerializerReverseManyToMany,
|
|
320
|
+
related_name="test_model_serializers",
|
|
321
|
+
filters={"name": (str, "")},
|
|
322
|
+
get_decorators=[track_get_decorator],
|
|
323
|
+
post_decorators=[track_post_decorator],
|
|
324
|
+
append_slash=True,
|
|
325
|
+
)
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
def test_model_serializers_query_params_handler(self, queryset, filters):
|
|
329
|
+
name_filter = filters.get("name")
|
|
330
|
+
if name_filter:
|
|
331
|
+
queryset = queryset.filter(name=name_filter)
|
|
332
|
+
return queryset
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@tag("many_to_many_api", "decorators")
|
|
336
|
+
class M2MRelationSchemaDecoratorsTestCase(Tests.BaseManyToManyAPITestCase):
|
|
337
|
+
"""Test M2M API with custom decorators applied to GET and POST endpoints."""
|
|
338
|
+
|
|
339
|
+
viewset_class = TestM2MWithDecoratorsViewSet
|
|
340
|
+
related_model = models.TestModelSerializerReverseManyToMany
|
|
341
|
+
related_name = "test_model_serializers"
|
|
342
|
+
api_namespace = "m2m_decorators_test"
|
|
343
|
+
related_names = ["dec_a", "dec_b", "dec_c"]
|
|
344
|
+
|
|
345
|
+
def setUp(self):
|
|
346
|
+
super().setUp()
|
|
347
|
+
# Reset decorator call counts before each test
|
|
348
|
+
_decorator_calls["get"] = 0
|
|
349
|
+
_decorator_calls["post"] = 0
|
|
350
|
+
|
|
351
|
+
async def test_get_decorator_is_applied(self):
|
|
352
|
+
"""Test that get_decorators are applied to the GET endpoint."""
|
|
353
|
+
# First add some related objects
|
|
354
|
+
await self._add_related()
|
|
355
|
+
initial_get_count = _decorator_calls["get"]
|
|
356
|
+
|
|
357
|
+
# Call the GET endpoint
|
|
358
|
+
await self.get_view(request=self.request.get(), pk=self.path_schema)
|
|
359
|
+
|
|
360
|
+
# Verify the get decorator was called
|
|
361
|
+
self.assertEqual(
|
|
362
|
+
_decorator_calls["get"],
|
|
363
|
+
initial_get_count + 1,
|
|
364
|
+
"GET decorator should have been called once",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
async def test_post_decorator_is_applied(self):
|
|
368
|
+
"""Test that post_decorators are applied to the POST endpoint."""
|
|
369
|
+
initial_post_count = _decorator_calls["post"]
|
|
370
|
+
|
|
371
|
+
# Call the POST endpoint to add related objects
|
|
372
|
+
await self._add_related()
|
|
373
|
+
|
|
374
|
+
# Verify the post decorator was called
|
|
375
|
+
self.assertEqual(
|
|
376
|
+
_decorator_calls["post"],
|
|
377
|
+
initial_post_count + 1,
|
|
378
|
+
"POST decorator should have been called once",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
async def test_decorators_independent(self):
|
|
382
|
+
"""Test that GET and POST decorators are applied independently."""
|
|
383
|
+
# Reset counts
|
|
384
|
+
_decorator_calls["get"] = 0
|
|
385
|
+
_decorator_calls["post"] = 0
|
|
386
|
+
|
|
387
|
+
# Call POST endpoint
|
|
388
|
+
await self._add_related()
|
|
389
|
+
self.assertEqual(_decorator_calls["post"], 1)
|
|
390
|
+
self.assertEqual(_decorator_calls["get"], 0)
|
|
391
|
+
|
|
392
|
+
# Call GET endpoint
|
|
393
|
+
await self.get_view(request=self.request.get(), pk=self.path_schema)
|
|
394
|
+
self.assertEqual(_decorator_calls["post"], 1) # Still 1
|
|
395
|
+
self.assertEqual(_decorator_calls["get"], 1)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@tag("many_to_many_api", "decorators", "schema")
|
|
399
|
+
class M2MRelationSchemaDecoratorsFieldTestCase(TestCase):
|
|
400
|
+
"""Test cases for M2MRelationSchema decorator fields."""
|
|
401
|
+
|
|
402
|
+
def test_decorators_default_to_empty_list(self):
|
|
403
|
+
"""Test that decorators default to empty lists."""
|
|
404
|
+
schema = M2MRelationSchema(
|
|
405
|
+
model=models.TestModelSerializerReverseManyToMany,
|
|
406
|
+
related_name="test_model_serializers",
|
|
407
|
+
)
|
|
408
|
+
self.assertEqual(schema.get_decorators, [])
|
|
409
|
+
self.assertEqual(schema.post_decorators, [])
|
|
410
|
+
|
|
411
|
+
def test_decorators_accept_list_of_callables(self):
|
|
412
|
+
"""Test that decorators accept a list of callables."""
|
|
413
|
+
|
|
414
|
+
def custom_decorator(func):
|
|
415
|
+
return func
|
|
416
|
+
|
|
417
|
+
schema = M2MRelationSchema(
|
|
418
|
+
model=models.TestModelSerializerReverseManyToMany,
|
|
419
|
+
related_name="test_model_serializers",
|
|
420
|
+
get_decorators=[custom_decorator],
|
|
421
|
+
post_decorators=[custom_decorator, custom_decorator],
|
|
422
|
+
)
|
|
423
|
+
self.assertEqual(len(schema.get_decorators), 1)
|
|
424
|
+
self.assertEqual(len(schema.post_decorators), 2)
|
|
425
|
+
self.assertEqual(schema.get_decorators[0], custom_decorator)
|
|
426
|
+
|
|
427
|
+
def test_decorators_can_be_none(self):
|
|
428
|
+
"""Test that decorators can explicitly be set to None."""
|
|
429
|
+
schema = M2MRelationSchema(
|
|
430
|
+
model=models.TestModelSerializerReverseManyToMany,
|
|
431
|
+
related_name="test_model_serializers",
|
|
432
|
+
get_decorators=None,
|
|
433
|
+
post_decorators=None,
|
|
434
|
+
)
|
|
435
|
+
# None values should be accepted
|
|
436
|
+
self.assertIsNone(schema.get_decorators)
|
|
437
|
+
self.assertIsNone(schema.post_decorators)
|
|
@@ -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.15.1 → 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.15.1 → 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.15.1 → django_ninja_aio_crud-2.16.1}/docs/api/models/serializers.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.15.1 → 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.15.1 → 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.15.1 → 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.15.1 → django_ninja_aio_crud-2.16.1}/ninja_aio/decorators/__init__.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.15.1 → 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.15.1 → 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
|
{django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/core/test_exceptions_api.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.15.1 → 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
|
|
File without changes
|
{django_ninja_aio_crud-2.15.1 → django_ninja_aio_crud-2.16.1}/tests/models/test_model_util.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.15.1 → 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
|