django-ninja-aio-crud 2.9.0__tar.gz → 2.10.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.9.0 → django_ninja_aio_crud-2.10.0}/.github/workflows/docs.yml +4 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/views/mixins.md +43 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/schemas/__init__.py +2 -0
- django_ninja_aio_crud-2.10.0/ninja_aio/schemas/api.py +43 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/views/mixins.py +63 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/core/test_renderer_parser.py +25 -0
- django_ninja_aio_crud-2.10.0/tests/models/test_model_util.py +152 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_app/serializers.py +16 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_app/views.py +25 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_auth.py +51 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_query_util.py +22 -0
- django_ninja_aio_crud-2.10.0/tests/views/test_views.py +301 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/views/test_viewset.py +167 -0
- django_ninja_aio_crud-2.9.0/ninja_aio/schemas/api.py +0 -24
- django_ninja_aio_crud-2.9.0/tests/models/test_model_util.py +0 -68
- django_ninja_aio_crud-2.9.0/tests/views/test_views.py +0 -144
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/README.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/models/serializers.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/views/api_view_set.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/main.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/models/serializers.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/models/utils.py +2 -2
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_app/models.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_serializers.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/views/__init__.py +0 -0
|
@@ -21,6 +21,8 @@ on:
|
|
|
21
21
|
- "2.6.1"
|
|
22
22
|
- "2.7.0"
|
|
23
23
|
- "2.8"
|
|
24
|
+
- "2.9"
|
|
25
|
+
- "2.10"
|
|
24
26
|
make_latest:
|
|
25
27
|
description: 'Set as "latest" and default?'
|
|
26
28
|
type: boolean
|
|
@@ -45,6 +47,8 @@ on:
|
|
|
45
47
|
- "2.6.1"
|
|
46
48
|
- "2.7.0"
|
|
47
49
|
- "2.8"
|
|
50
|
+
- "2.9"
|
|
51
|
+
- "2.10"
|
|
48
52
|
delete_confirm:
|
|
49
53
|
description: 'Confirm deletion of the selected version'
|
|
50
54
|
type: boolean
|
|
@@ -140,6 +140,49 @@ class EventViewSet(LessEqualDateFilterViewSetMixin, APIViewSet):
|
|
|
140
140
|
query_params = {"created_at": (datetime, None)}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
## RelationFilterViewSetMixin
|
|
144
|
+
|
|
145
|
+
Filters by related model fields using configurable `RelationFilterSchema` entries.
|
|
146
|
+
|
|
147
|
+
- Behavior: Maps query parameters to Django ORM lookups on related models.
|
|
148
|
+
- Configuration: Define `relations_filters` as a list of `RelationFilterSchema` objects.
|
|
149
|
+
- Query params are automatically registered from `relations_filters`.
|
|
150
|
+
|
|
151
|
+
Each `RelationFilterSchema` requires:
|
|
152
|
+
|
|
153
|
+
- `query_param`: The API query parameter name exposed to clients.
|
|
154
|
+
- `query_filter`: The Django ORM lookup path (e.g., `author__id`, `category__name__icontains`).
|
|
155
|
+
- `filter_type`: Tuple of `(type, default)` for schema generation (e.g., `(int, None)`).
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from ninja_aio.views.mixins import RelationFilterViewSetMixin
|
|
161
|
+
from ninja_aio.views.api import APIViewSet
|
|
162
|
+
from ninja_aio.schemas import RelationFilterSchema
|
|
163
|
+
|
|
164
|
+
class BookViewSet(RelationFilterViewSetMixin, APIViewSet):
|
|
165
|
+
model = models.Book
|
|
166
|
+
api = api
|
|
167
|
+
relations_filters = [
|
|
168
|
+
RelationFilterSchema(
|
|
169
|
+
query_param="author",
|
|
170
|
+
query_filter="author__id",
|
|
171
|
+
filter_type=(int, None),
|
|
172
|
+
),
|
|
173
|
+
RelationFilterSchema(
|
|
174
|
+
query_param="category_name",
|
|
175
|
+
query_filter="category__name__icontains",
|
|
176
|
+
filter_type=(str, None),
|
|
177
|
+
),
|
|
178
|
+
]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This enables:
|
|
182
|
+
|
|
183
|
+
- `GET /books?author=5` → `queryset.filter(author__id=5)`
|
|
184
|
+
- `GET /books?category_name=fiction` → `queryset.filter(category__name__icontains="fiction")`
|
|
185
|
+
|
|
143
186
|
## Tips
|
|
144
187
|
|
|
145
188
|
- Align `query_params` types with expected filter values; prefer Pydantic `date`/`datetime` for date filters so values implement `isoformat`.
|
|
@@ -5,6 +5,7 @@ from .api import (
|
|
|
5
5
|
M2MAddSchemaIn,
|
|
6
6
|
M2MRemoveSchemaIn,
|
|
7
7
|
M2MSchemaIn,
|
|
8
|
+
RelationFilterSchema,
|
|
8
9
|
)
|
|
9
10
|
from .helpers import M2MRelationSchema, QuerySchema, ModelQuerySetSchema, ObjectQuerySchema, ObjectsQuerySchema
|
|
10
11
|
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
|
20
21
|
"ModelQuerySetSchema",
|
|
21
22
|
"ObjectQuerySchema",
|
|
22
23
|
"ObjectsQuerySchema",
|
|
24
|
+
"RelationFilterSchema",
|
|
23
25
|
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ninja import Schema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class M2MDetailSchema(Schema):
|
|
7
|
+
count: int
|
|
8
|
+
details: list[str]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class M2MSchemaOut(Schema):
|
|
12
|
+
errors: M2MDetailSchema
|
|
13
|
+
results: M2MDetailSchema
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class M2MAddSchemaIn(Schema):
|
|
17
|
+
add: list = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class M2MRemoveSchemaIn(Schema):
|
|
21
|
+
remove: list = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class M2MSchemaIn(Schema):
|
|
25
|
+
add: list = []
|
|
26
|
+
remove: list = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RelationFilterSchema(Schema):
|
|
30
|
+
"""
|
|
31
|
+
Schema for configuring relation-based filters in RelationFilterViewSetMixin.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
filter_type: A tuple of (type, default_value) used to generate the query parameter
|
|
35
|
+
field in the filters schema. Example: (int, None) for optional integer filter.
|
|
36
|
+
query_param: The name of the query parameter exposed in the API endpoint.
|
|
37
|
+
This is what clients will use in requests (e.g., ?author_id=5).
|
|
38
|
+
query_filter: The Django ORM lookup to apply (e.g., "author__id", "category__slug").
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
filter_type: tuple[type, Any]
|
|
42
|
+
query_param: str
|
|
43
|
+
query_filter: str
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from ninja_aio.views.api import APIViewSet
|
|
2
|
+
from ninja_aio.schemas import RelationFilterSchema
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class IcontainsFilterViewSetMixin(APIViewSet):
|
|
@@ -273,3 +274,65 @@ class LessEqualDateFilterViewSetMixin(DateFilterViewSetMixin):
|
|
|
273
274
|
"""
|
|
274
275
|
|
|
275
276
|
_compare_attr = "__lte"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class RelationFilterViewSetMixin(APIViewSet):
|
|
280
|
+
"""
|
|
281
|
+
Mixin providing filtering for related fields in Django QuerySets.
|
|
282
|
+
|
|
283
|
+
This mixin applies filters on related fields based on configured RelationFilterSchema
|
|
284
|
+
entries. Each entry maps a query parameter name to a Django ORM lookup path.
|
|
285
|
+
|
|
286
|
+
Attributes:
|
|
287
|
+
relations_filters: List of RelationFilterSchema defining the relation filters.
|
|
288
|
+
Each schema specifies:
|
|
289
|
+
- query_param: The API query parameter name (e.g., "author_id")
|
|
290
|
+
- query_filter: The Django ORM lookup (e.g., "author__id")
|
|
291
|
+
- filter_type: Tuple of (type, default) for schema generation
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
class BookViewSet(RelationFilterViewSetMixin, APIViewSet):
|
|
295
|
+
relations_filters = [
|
|
296
|
+
RelationFilterSchema(
|
|
297
|
+
query_param="author_id",
|
|
298
|
+
query_filter="author__id",
|
|
299
|
+
filter_type=(int, None),
|
|
300
|
+
),
|
|
301
|
+
RelationFilterSchema(
|
|
302
|
+
query_param="category_slug",
|
|
303
|
+
query_filter="category__slug",
|
|
304
|
+
filter_type=(str, None),
|
|
305
|
+
),
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
# GET /books?author_id=5 -> queryset.filter(author__id=5)
|
|
309
|
+
# GET /books?category_slug=fiction -> queryset.filter(category__slug="fiction")
|
|
310
|
+
|
|
311
|
+
Notes:
|
|
312
|
+
- Filter values that are None or falsy are skipped.
|
|
313
|
+
- This mixin automatically registers query_params from relations_filters.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
relations_filters: list[RelationFilterSchema] = []
|
|
317
|
+
|
|
318
|
+
def __init_subclass__(cls, **kwargs):
|
|
319
|
+
super().__init_subclass__(**kwargs)
|
|
320
|
+
cls.query_params = {
|
|
321
|
+
**cls.query_params,
|
|
322
|
+
**{
|
|
323
|
+
rel_filter.query_param: rel_filter.filter_type
|
|
324
|
+
for rel_filter in cls.relations_filters
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async def query_params_handler(self, queryset, filters):
|
|
329
|
+
"""
|
|
330
|
+
Apply relation filters to the queryset based on configured relations_filters.
|
|
331
|
+
"""
|
|
332
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
333
|
+
rel_filters = {}
|
|
334
|
+
for rel_filter in self.relations_filters:
|
|
335
|
+
value = filters.get(rel_filter.query_param)
|
|
336
|
+
if value is not None:
|
|
337
|
+
rel_filters[rel_filter.query_filter] = value
|
|
338
|
+
return base_qs.filter(**rel_filters) if rel_filters else base_qs
|
{django_ninja_aio_crud-2.9.0 → django_ninja_aio_crud-2.10.0}/tests/core/test_renderer_parser.py
RENAMED
|
@@ -48,3 +48,28 @@ class ORJSONRendererParserTestCase(TestCase):
|
|
|
48
48
|
[base64.b64encode(b"a").decode(), base64.b64encode(b"b").decode()],
|
|
49
49
|
)
|
|
50
50
|
self.assertEqual(decoded["plain"], "value")
|
|
51
|
+
|
|
52
|
+
def test_renderer_non_dict_data(self):
|
|
53
|
+
"""Test that renderer handles non-dict data (covers lines 23-24)."""
|
|
54
|
+
# When data is not a dict, it triggers the AttributeError branch
|
|
55
|
+
# and falls back to self.dumps(data)
|
|
56
|
+
non_dict_data = "plain string"
|
|
57
|
+
rendered = self.renderer.render(DummyRequest(), non_dict_data, response_status=200)
|
|
58
|
+
decoded = orjson.loads(rendered)
|
|
59
|
+
self.assertEqual(decoded, "plain string")
|
|
60
|
+
|
|
61
|
+
def test_renderer_list_data(self):
|
|
62
|
+
"""Test that renderer handles list data directly."""
|
|
63
|
+
# A list doesn't have .items() method
|
|
64
|
+
list_data = [1, 2, 3]
|
|
65
|
+
rendered = self.renderer.render(DummyRequest(), list_data, response_status=200)
|
|
66
|
+
decoded = orjson.loads(rendered)
|
|
67
|
+
self.assertEqual(decoded, [1, 2, 3])
|
|
68
|
+
|
|
69
|
+
def test_renderer_primitive_data(self):
|
|
70
|
+
"""Test that renderer handles primitive data."""
|
|
71
|
+
# An integer doesn't have .items() method
|
|
72
|
+
int_data = 42
|
|
73
|
+
rendered = self.renderer.render(DummyRequest(), int_data, response_status=200)
|
|
74
|
+
decoded = orjson.loads(rendered)
|
|
75
|
+
self.assertEqual(decoded, 42)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from django.test import tag, TestCase
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
from ninja.errors import ConfigError
|
|
5
|
+
from ninja_aio.models import ModelUtil
|
|
6
|
+
from tests.test_app import models, schema
|
|
7
|
+
from tests.generics.models import Tests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseTests:
|
|
11
|
+
class ModelUtilTestCaseBase(Tests.GenericModelUtilTestCase):
|
|
12
|
+
@property
|
|
13
|
+
def create_data(self):
|
|
14
|
+
return {"name": "test", "description": "test"}
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def parsed_input_data(self):
|
|
18
|
+
return {"payload": self.create_data}
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def read_data(self):
|
|
22
|
+
return {"id": 1, "name": "test", "description": "test"}
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def additional_getters(self):
|
|
26
|
+
return {"description": "test"}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def additional_filters(self):
|
|
30
|
+
return {"name": "test"}
|
|
31
|
+
|
|
32
|
+
@tag("model_util_model_serializer")
|
|
33
|
+
class ModelUtilModelSerializerTestCase(ModelUtilTestCaseBase):
|
|
34
|
+
@property
|
|
35
|
+
def serializable_fields(self):
|
|
36
|
+
return self.model.ReadSerializer.fields
|
|
37
|
+
|
|
38
|
+
@tag("model_util_model")
|
|
39
|
+
class ModelUtilModelBaseTestCase(ModelUtilTestCaseBase):
|
|
40
|
+
@property
|
|
41
|
+
def serializable_fields(self):
|
|
42
|
+
return ["id", "name", "description"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@tag("model_util_model_serializer_base")
|
|
46
|
+
class ModelUtilModelSerializerBaseTestCase(BaseTests.ModelUtilModelSerializerTestCase):
|
|
47
|
+
model = models.TestModelSerializer
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def model_verbose_name_path(self):
|
|
51
|
+
return "test-model-serializers"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def model_verbose_name_view(self):
|
|
55
|
+
return "testmodelserializers"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@tag("model_util_model_base")
|
|
59
|
+
class ModelUtilModelBaseTestCase(BaseTests.ModelUtilModelBaseTestCase):
|
|
60
|
+
model = models.TestModel
|
|
61
|
+
schema_in = schema.TestModelSchemaIn
|
|
62
|
+
schema_out = schema.TestModelSchemaOut
|
|
63
|
+
schema_patch = schema.TestModelSchemaPatch
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def model_verbose_name_path(self):
|
|
67
|
+
return "test-models"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def model_verbose_name_view(self):
|
|
71
|
+
return "testmodels"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@tag("model_util_config_error")
|
|
75
|
+
class ModelUtilConfigErrorTestCase(TestCase):
|
|
76
|
+
"""Test ModelUtil ConfigError edge cases (covers lines 113-115)."""
|
|
77
|
+
|
|
78
|
+
def test_model_util_raises_config_error_for_model_serializer_with_serializer_class(
|
|
79
|
+
self,
|
|
80
|
+
):
|
|
81
|
+
"""Test that ModelUtil raises ConfigError when both model is ModelSerializer and serializer_class is provided."""
|
|
82
|
+
from tests.test_app.serializers import TestModelForeignKeySerializer
|
|
83
|
+
|
|
84
|
+
with self.assertRaises(ConfigError) as ctx:
|
|
85
|
+
ModelUtil(
|
|
86
|
+
models.TestModelSerializer,
|
|
87
|
+
serializer_class=TestModelForeignKeySerializer,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self.assertIn(
|
|
91
|
+
"cannot accept both model and serializer_class", str(ctx.exception)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@tag("model_util_pk_field_type")
|
|
96
|
+
class ModelUtilPkFieldTypeTestCase(TestCase):
|
|
97
|
+
"""Test ModelUtil pk_field_type error handling (covers lines 152-158)."""
|
|
98
|
+
|
|
99
|
+
def test_pk_field_type_raises_config_error_for_unknown_type(self):
|
|
100
|
+
"""Test that pk_field_type raises ConfigError for unknown field types."""
|
|
101
|
+
from ninja.orm import fields
|
|
102
|
+
|
|
103
|
+
# Create a mock model with an unknown pk field type
|
|
104
|
+
mock_pk_field = mock.Mock()
|
|
105
|
+
mock_pk_field.get_internal_type.return_value = "UnknownFieldType"
|
|
106
|
+
|
|
107
|
+
mock_meta = mock.Mock()
|
|
108
|
+
mock_meta.pk = mock_pk_field
|
|
109
|
+
|
|
110
|
+
mock_model = mock.Mock()
|
|
111
|
+
mock_model._meta = mock_meta
|
|
112
|
+
|
|
113
|
+
util = ModelUtil(mock_model)
|
|
114
|
+
|
|
115
|
+
# Temporarily remove the key if it exists to ensure KeyError
|
|
116
|
+
original_types = fields.TYPES.copy()
|
|
117
|
+
fields.TYPES.pop("UnknownFieldType", None)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
with self.assertRaises(ConfigError) as ctx:
|
|
121
|
+
_ = util.pk_field_type
|
|
122
|
+
|
|
123
|
+
self.assertIn("Do not know how to convert", str(ctx.exception))
|
|
124
|
+
self.assertIn("UnknownFieldType", str(ctx.exception))
|
|
125
|
+
finally:
|
|
126
|
+
# Restore original types
|
|
127
|
+
fields.TYPES.update(original_types)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@tag("model_util_objects_query_default")
|
|
131
|
+
class ModelUtilObjectsQueryDefaultTestCase(TestCase):
|
|
132
|
+
"""Test ModelUtil get_objects with default ObjectsQuerySchema (covers line 351)."""
|
|
133
|
+
|
|
134
|
+
async def test_get_objects_with_none_query_data_uses_default(self):
|
|
135
|
+
"""Test that get_objects uses default ObjectsQuerySchema when query_data is None."""
|
|
136
|
+
# Create a test object
|
|
137
|
+
obj = await models.TestModel.objects.acreate(name="test", description="desc")
|
|
138
|
+
|
|
139
|
+
util = ModelUtil(models.TestModel)
|
|
140
|
+
|
|
141
|
+
# Create a mock request
|
|
142
|
+
request = mock.Mock()
|
|
143
|
+
|
|
144
|
+
# Call get_objects with query_data=None (will use default ObjectsQuerySchema)
|
|
145
|
+
qs = await util.get_objects(request, query_data=None)
|
|
146
|
+
|
|
147
|
+
# Should return a queryset
|
|
148
|
+
count = await qs.acount()
|
|
149
|
+
self.assertGreaterEqual(count, 1)
|
|
150
|
+
|
|
151
|
+
# Cleanup
|
|
152
|
+
await obj.adelete()
|
|
@@ -29,3 +29,19 @@ class TestModelReverseForeignKeySerializer(serializers.Serializer):
|
|
|
29
29
|
relations_serializers = {
|
|
30
30
|
"test_model_foreign_keys": TestModelForeignKeySerializer,
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestModelOneToOneSerializer(serializers.Serializer):
|
|
35
|
+
"""Serializer for TestModelOneToOne to test ForwardOneToOneDescriptor coverage."""
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
model = models.TestModelOneToOne
|
|
39
|
+
schema_in = serializers.SchemaModelConfig(
|
|
40
|
+
fields=["name", "description", "test_model"]
|
|
41
|
+
)
|
|
42
|
+
schema_out = serializers.SchemaModelConfig(
|
|
43
|
+
fields=["id", "name", "description", "test_model"]
|
|
44
|
+
)
|
|
45
|
+
schema_update = serializers.SchemaModelConfig(
|
|
46
|
+
optionals=[("name", str), ("description", str)]
|
|
47
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from ninja_aio.views import mixins
|
|
3
|
+
from ninja_aio.schemas import RelationFilterSchema
|
|
3
4
|
|
|
4
5
|
from tests.generics.views import GenericAPIViewSet
|
|
5
6
|
from tests.test_app import models, schema, serializers
|
|
@@ -153,3 +154,27 @@ class TestModelForeignKeySerializerAPI(GenericAPIViewSet):
|
|
|
153
154
|
class TestModelReverseForeignKeySerializerAPI(GenericAPIViewSet):
|
|
154
155
|
model = models.TestModelReverseForeignKey
|
|
155
156
|
serializer_class = serializers.TestModelReverseForeignKeySerializer
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ==========================================================
|
|
160
|
+
# RELATION FILTER MIXIN APIS
|
|
161
|
+
# ==========================================================
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestModelSerializerForeignKeyRelationFilterAPI(
|
|
165
|
+
GenericAPIViewSet,
|
|
166
|
+
mixins.RelationFilterViewSetMixin,
|
|
167
|
+
):
|
|
168
|
+
model = models.TestModelSerializerForeignKey
|
|
169
|
+
relations_filters = [
|
|
170
|
+
RelationFilterSchema(
|
|
171
|
+
query_param="test_model_serializer",
|
|
172
|
+
query_filter="test_model_serializer__id",
|
|
173
|
+
filter_type=(int, None),
|
|
174
|
+
),
|
|
175
|
+
RelationFilterSchema(
|
|
176
|
+
query_param="test_model_serializer_name",
|
|
177
|
+
query_filter="test_model_serializer__name__icontains",
|
|
178
|
+
filter_type=(str, None),
|
|
179
|
+
),
|
|
180
|
+
]
|
|
@@ -122,3 +122,54 @@ class JwtAuthTests(TestCase):
|
|
|
122
122
|
bearer = TBWrongAud()
|
|
123
123
|
result = async_to_sync(bearer.authenticate)(HttpRequest(), token)
|
|
124
124
|
self.assertFalse(result)
|
|
125
|
+
|
|
126
|
+
def test_async_bearer_authenticate_invalid_token_returns_false(self):
|
|
127
|
+
"""Test that a malformed/invalid token returns False (covers lines 110-112)."""
|
|
128
|
+
pub = self.public_jwk
|
|
129
|
+
|
|
130
|
+
class TB(AsyncJwtBearer):
|
|
131
|
+
jwt_public = pub
|
|
132
|
+
claims = {
|
|
133
|
+
"iss": {"value": "test-issuer"},
|
|
134
|
+
"aud": {"value": "test-audience"},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async def auth_handler(self, request):
|
|
138
|
+
return "should-not-happen"
|
|
139
|
+
|
|
140
|
+
bearer = TB()
|
|
141
|
+
# The ValueError branch at 110-112 is for when jwt.decode raises ValueError
|
|
142
|
+
# Mock jwt.decode to raise ValueError to test that branch
|
|
143
|
+
import unittest.mock as mock
|
|
144
|
+
|
|
145
|
+
with mock.patch("ninja_aio.auth.jwt.decode") as mock_decode:
|
|
146
|
+
mock_decode.side_effect = ValueError("invalid token")
|
|
147
|
+
result = async_to_sync(bearer.authenticate)(HttpRequest(), "fake-token")
|
|
148
|
+
self.assertFalse(result)
|
|
149
|
+
|
|
150
|
+
def test_async_bearer_base_auth_handler_returns_none(self):
|
|
151
|
+
"""Test that the base auth_handler returns None (covers line 101)."""
|
|
152
|
+
pub = self.public_jwk
|
|
153
|
+
|
|
154
|
+
class TB(AsyncJwtBearer):
|
|
155
|
+
jwt_public = pub
|
|
156
|
+
claims = {
|
|
157
|
+
"iss": {"value": "test-issuer"},
|
|
158
|
+
"aud": {"value": "test-audience"},
|
|
159
|
+
}
|
|
160
|
+
# Don't override auth_handler, use base implementation
|
|
161
|
+
|
|
162
|
+
token = encode_jwt({"sub": "42"}, duration=60)
|
|
163
|
+
bearer = TB()
|
|
164
|
+
result = async_to_sync(bearer.authenticate)(HttpRequest(), token)
|
|
165
|
+
# Base auth_handler returns None (pass statement)
|
|
166
|
+
self.assertIsNone(result)
|
|
167
|
+
|
|
168
|
+
def test_validate_mandatory_claims_skips_preset_claims(self):
|
|
169
|
+
"""Test that validate_mandatory_claims skips claims already present (covers line 137)."""
|
|
170
|
+
# Pre-set both mandatory claims
|
|
171
|
+
claims = {"iss": "custom-issuer", "aud": "custom-audience"}
|
|
172
|
+
result = validate_mandatory_claims(claims)
|
|
173
|
+
# Should keep the pre-set values, not override with settings
|
|
174
|
+
self.assertEqual(result["iss"], "custom-issuer")
|
|
175
|
+
self.assertEqual(result["aud"], "custom-audience")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from unittest import mock
|
|
2
2
|
from django.test import TestCase, tag
|
|
3
3
|
from tests.test_app import models as app_models
|
|
4
|
+
from ninja_aio.helpers.query import ScopeNamespace
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@tag("query_util_dedicated")
|
|
@@ -93,3 +94,24 @@ class QueryUtilDedicatedTestCase(TestCase):
|
|
|
93
94
|
_ = util_simple.apply_queryset_optimizations(qs, util_simple.SCOPES.READ)
|
|
94
95
|
m_sel.assert_not_called()
|
|
95
96
|
m_pref.assert_not_called()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@tag("scope_namespace")
|
|
100
|
+
class ScopeNamespaceTestCase(TestCase):
|
|
101
|
+
"""Tests for ScopeNamespace class (covers line 17 - __iter__)."""
|
|
102
|
+
|
|
103
|
+
def test_scope_namespace_iter(self):
|
|
104
|
+
"""Test that ScopeNamespace is iterable (covers line 17)."""
|
|
105
|
+
namespace = ScopeNamespace(READ="read", WRITE="write", CUSTOM="custom")
|
|
106
|
+
# Test iteration
|
|
107
|
+
values = list(namespace)
|
|
108
|
+
self.assertEqual(len(values), 3)
|
|
109
|
+
self.assertIn("read", values)
|
|
110
|
+
self.assertIn("write", values)
|
|
111
|
+
self.assertIn("custom", values)
|
|
112
|
+
|
|
113
|
+
def test_scope_namespace_attributes(self):
|
|
114
|
+
"""Test that ScopeNamespace sets attributes correctly."""
|
|
115
|
+
namespace = ScopeNamespace(FOO="foo", BAR="bar")
|
|
116
|
+
self.assertEqual(namespace.FOO, "foo")
|
|
117
|
+
self.assertEqual(namespace.BAR, "bar")
|