django-ninja-aio-crud 2.10.1__tar.gz → 2.11.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.
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/.github/workflows/docs.yml +2 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/PKG-INFO +1 -1
- django_ninja_aio_crud-2.11.1/docs/api/renderers/orjson_renderer.md +57 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/views/mixins.md +83 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/renders.py +3 -1
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/schemas/__init__.py +15 -1
- django_ninja_aio_crud-2.11.1/ninja_aio/schemas/api.py +24 -0
- django_ninja_aio_crud-2.11.1/ninja_aio/schemas/filters.py +73 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/views/api.py +8 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/views/mixins.py +72 -7
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/core/test_renderer_parser.py +23 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_app/models.py +1 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_app/views.py +65 -1
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/views/test_viewset.py +160 -0
- django_ninja_aio_crud-2.10.1/docs/api/renderers/orjson_renderer.md +0 -20
- django_ninja_aio_crud-2.10.1/ninja_aio/schemas/api.py +0 -43
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/.gitignore +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/LICENSE +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/README.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/models/serializers.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/views/api_view_set.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/main.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/models/serializers.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_serializers.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/views/test_views.py +0 -0
|
@@ -23,6 +23,7 @@ on:
|
|
|
23
23
|
- "2.8"
|
|
24
24
|
- "2.9"
|
|
25
25
|
- "2.10"
|
|
26
|
+
- "2.11"
|
|
26
27
|
make_latest:
|
|
27
28
|
description: 'Set as "latest" and default?'
|
|
28
29
|
type: boolean
|
|
@@ -49,6 +50,7 @@ on:
|
|
|
49
50
|
- "2.8"
|
|
50
51
|
- "2.9"
|
|
51
52
|
- "2.10"
|
|
53
|
+
- "2.11"
|
|
52
54
|
delete_confirm:
|
|
53
55
|
description: 'Confirm deletion of the selected version'
|
|
54
56
|
type: boolean
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ORJSON Renderer
|
|
2
|
+
|
|
3
|
+
This package uses an internal ORJSON-based renderer that automatically handles JSON serialization with support for special types.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Configure serialization options via Django settings:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# settings.py
|
|
11
|
+
import orjson
|
|
12
|
+
|
|
13
|
+
# Single option
|
|
14
|
+
NINJA_AIO_ORJSON_RENDERER_OPTION = orjson.OPT_INDENT_2
|
|
15
|
+
|
|
16
|
+
# Multiple options (bitwise OR)
|
|
17
|
+
NINJA_AIO_ORJSON_RENDERER_OPTION = (
|
|
18
|
+
orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Notes:
|
|
23
|
+
|
|
24
|
+
- The value is an orjson option bitmask (e.g., `orjson.OPT_INDENT_2`, `orjson.OPT_NON_STR_KEYS`), and you can combine multiple options using `|`.
|
|
25
|
+
- If not set, the default `orjson.dumps` options are used.
|
|
26
|
+
|
|
27
|
+
## HttpResponse Passthrough
|
|
28
|
+
|
|
29
|
+
The renderer automatically detects when you return a Django `HttpResponse` (or any `HttpResponseBase` subclass) and passes it through without JSON serialization. This allows you to return custom responses with different content types.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from django.http import HttpResponse
|
|
33
|
+
|
|
34
|
+
@api.get("/public-key")
|
|
35
|
+
def get_public_key(request):
|
|
36
|
+
return HttpResponse(
|
|
37
|
+
settings.JWT_PUBLIC_KEY.as_pem(),
|
|
38
|
+
content_type="application/x-pem-file",
|
|
39
|
+
status=200,
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This also works with `StreamingHttpResponse` for large files:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from django.http import StreamingHttpResponse
|
|
47
|
+
|
|
48
|
+
@api.get("/download")
|
|
49
|
+
def download_file(request):
|
|
50
|
+
return StreamingHttpResponse(
|
|
51
|
+
file_iterator(),
|
|
52
|
+
content_type="application/octet-stream",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
!!! note
|
|
57
|
+
When returning an `HttpResponse` directly, the response bypasses the renderer entirely. Set the `status` parameter on the `HttpResponse` itself rather than using a tuple return like `return 200, HttpResponse(...)`.
|
|
@@ -183,6 +183,89 @@ This enables:
|
|
|
183
183
|
- `GET /books?author=5` → `queryset.filter(author__id=5)`
|
|
184
184
|
- `GET /books?category_name=fiction` → `queryset.filter(category__name__icontains="fiction")`
|
|
185
185
|
|
|
186
|
+
## MatchCaseFilterViewSetMixin
|
|
187
|
+
|
|
188
|
+
Applies conditional filtering based on boolean query parameters, where different filter conditions are applied for `True` and `False` values. This is useful when you need to map a simple boolean API parameter to complex underlying filter logic.
|
|
189
|
+
|
|
190
|
+
- Behavior: For each `MatchCaseFilterSchema` entry, applies different Django ORM filters based on the boolean value of the query parameter.
|
|
191
|
+
- Configuration: Define `filters_match_cases` as a list of `MatchCaseFilterSchema` objects.
|
|
192
|
+
- Query params are automatically registered from `filters_match_cases`.
|
|
193
|
+
- Supports both `filter()` (include) and `exclude()` operations via the `include` attribute.
|
|
194
|
+
|
|
195
|
+
Each `MatchCaseFilterSchema` requires:
|
|
196
|
+
|
|
197
|
+
- `query_param`: The API query parameter name exposed to clients.
|
|
198
|
+
- `cases`: A `BooleanMatchFilterSchema` defining the filter conditions for `True` and `False` cases.
|
|
199
|
+
|
|
200
|
+
Each `MatchConditionFilterSchema` (used within `BooleanMatchFilterSchema`) requires:
|
|
201
|
+
|
|
202
|
+
- `query_filter`: A dictionary of Django ORM lookups to apply (e.g., `{"status": "active"}`).
|
|
203
|
+
- `include`: Boolean indicating whether to use `filter()` (True) or `exclude()` (False). Defaults to `True`.
|
|
204
|
+
|
|
205
|
+
Example - Simple status filtering:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from ninja_aio.views.mixins import MatchCaseFilterViewSetMixin
|
|
209
|
+
from ninja_aio.views.api import APIViewSet
|
|
210
|
+
from ninja_aio.schemas import (
|
|
211
|
+
MatchCaseFilterSchema,
|
|
212
|
+
MatchConditionFilterSchema,
|
|
213
|
+
BooleanMatchFilterSchema,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
class OrderViewSet(MatchCaseFilterViewSetMixin, APIViewSet):
|
|
217
|
+
model = models.Order
|
|
218
|
+
api = api
|
|
219
|
+
filters_match_cases = [
|
|
220
|
+
MatchCaseFilterSchema(
|
|
221
|
+
query_param="is_completed",
|
|
222
|
+
cases=BooleanMatchFilterSchema(
|
|
223
|
+
true=MatchConditionFilterSchema(
|
|
224
|
+
query_filter={"status": "completed"},
|
|
225
|
+
include=True,
|
|
226
|
+
),
|
|
227
|
+
false=MatchConditionFilterSchema(
|
|
228
|
+
query_filter={"status": "completed"},
|
|
229
|
+
include=False, # excludes completed orders
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This enables:
|
|
237
|
+
|
|
238
|
+
- `GET /orders?is_completed=true` → `queryset.filter(status="completed")`
|
|
239
|
+
- `GET /orders?is_completed=false` → `queryset.exclude(status="completed")`
|
|
240
|
+
|
|
241
|
+
Example - Complex filtering with multiple conditions:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
class TaskViewSet(MatchCaseFilterViewSetMixin, APIViewSet):
|
|
245
|
+
model = models.Task
|
|
246
|
+
api = api
|
|
247
|
+
filters_match_cases = [
|
|
248
|
+
MatchCaseFilterSchema(
|
|
249
|
+
query_param="show_active",
|
|
250
|
+
cases=BooleanMatchFilterSchema(
|
|
251
|
+
true=MatchConditionFilterSchema(
|
|
252
|
+
query_filter={"status__in": ["pending", "in_progress"]},
|
|
253
|
+
include=True,
|
|
254
|
+
),
|
|
255
|
+
false=MatchConditionFilterSchema(
|
|
256
|
+
query_filter={"status__in": ["completed", "cancelled"]},
|
|
257
|
+
include=True,
|
|
258
|
+
),
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
This enables:
|
|
265
|
+
|
|
266
|
+
- `GET /tasks?show_active=true` → `queryset.filter(status__in=["pending", "in_progress"])`
|
|
267
|
+
- `GET /tasks?show_active=false` → `queryset.filter(status__in=["completed", "cancelled"])`
|
|
268
|
+
|
|
186
269
|
## Tips
|
|
187
270
|
|
|
188
271
|
- Align `query_params` types with expected filter values; prefer Pydantic `date`/`datetime` for date filters so values implement `isoformat`.
|
|
@@ -3,7 +3,7 @@ from ipaddress import IPv4Address, IPv6Address
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
import orjson
|
|
6
|
-
from django.http import HttpRequest
|
|
6
|
+
from django.http import HttpRequest, HttpResponseBase
|
|
7
7
|
from django.conf import settings
|
|
8
8
|
from ninja.renderers import BaseRenderer
|
|
9
9
|
from pydantic import AnyUrl
|
|
@@ -14,6 +14,8 @@ class ORJSONRenderer(BaseRenderer):
|
|
|
14
14
|
option = getattr(settings, "NINJA_AIO_ORJSON_RENDERER_OPTION", None)
|
|
15
15
|
|
|
16
16
|
def render(self, request: HttpRequest, data: dict, *, response_status):
|
|
17
|
+
if isinstance(data, HttpResponseBase):
|
|
18
|
+
return data
|
|
17
19
|
try:
|
|
18
20
|
old_d = data
|
|
19
21
|
for k, v in old_d.items():
|
|
@@ -5,9 +5,20 @@ from .api import (
|
|
|
5
5
|
M2MAddSchemaIn,
|
|
6
6
|
M2MRemoveSchemaIn,
|
|
7
7
|
M2MSchemaIn,
|
|
8
|
+
)
|
|
9
|
+
from .filters import (
|
|
8
10
|
RelationFilterSchema,
|
|
11
|
+
MatchCaseFilterSchema,
|
|
12
|
+
MatchConditionFilterSchema,
|
|
13
|
+
BooleanMatchFilterSchema,
|
|
14
|
+
)
|
|
15
|
+
from .helpers import (
|
|
16
|
+
M2MRelationSchema,
|
|
17
|
+
QuerySchema,
|
|
18
|
+
ModelQuerySetSchema,
|
|
19
|
+
ObjectQuerySchema,
|
|
20
|
+
ObjectsQuerySchema,
|
|
9
21
|
)
|
|
10
|
-
from .helpers import M2MRelationSchema, QuerySchema, ModelQuerySetSchema, ObjectQuerySchema, ObjectsQuerySchema
|
|
11
22
|
|
|
12
23
|
__all__ = [
|
|
13
24
|
"GenericMessageSchema",
|
|
@@ -22,4 +33,7 @@ __all__ = [
|
|
|
22
33
|
"ObjectQuerySchema",
|
|
23
34
|
"ObjectsQuerySchema",
|
|
24
35
|
"RelationFilterSchema",
|
|
36
|
+
"MatchCaseFilterSchema",
|
|
37
|
+
"MatchConditionFilterSchema",
|
|
38
|
+
"BooleanMatchFilterSchema",
|
|
25
39
|
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ninja import Schema
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class M2MDetailSchema(Schema):
|
|
5
|
+
count: int
|
|
6
|
+
details: list[str]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class M2MSchemaOut(Schema):
|
|
10
|
+
errors: M2MDetailSchema
|
|
11
|
+
results: M2MDetailSchema
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class M2MAddSchemaIn(Schema):
|
|
15
|
+
add: list = []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class M2MRemoveSchemaIn(Schema):
|
|
19
|
+
remove: list = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class M2MSchemaIn(Schema):
|
|
23
|
+
add: list = []
|
|
24
|
+
remove: list = []
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ninja import Schema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FilterSchema(Schema):
|
|
7
|
+
"""
|
|
8
|
+
Schema for configuring basic filters in FilterViewSetMixin.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
filter_type: A tuple of (type, default_value) used to generate the query parameter
|
|
12
|
+
field in the filters schema. Example: (str, None) for optional string filter.
|
|
13
|
+
query_param: The name of the query parameter exposed in the API endpoint.
|
|
14
|
+
This is what clients will use in requests (e.g., ?name=example).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
filter_type: tuple[type, Any]
|
|
18
|
+
query_param: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MatchConditionFilterSchema(Schema):
|
|
22
|
+
"""
|
|
23
|
+
Schema for configuring match condition filters in MatchConditionFilterViewSetMixin.
|
|
24
|
+
Attributes:
|
|
25
|
+
query_filter: The Django ORM lookup to apply (e.g., "status", "category__name").
|
|
26
|
+
include: Whether to include records matching the condition (default: True).
|
|
27
|
+
"""
|
|
28
|
+
query_filter: dict[str, Any]
|
|
29
|
+
include: bool = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BooleanMatchFilterSchema(Schema):
|
|
33
|
+
"""
|
|
34
|
+
Schema for configuring boolean match filters in BooleanMatchFilterViewSetMixin.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
true: MatchConditionFilterSchema for when the boolean filter is True.
|
|
38
|
+
false: MatchConditionFilterSchema for when the boolean filter is False.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
true: MatchConditionFilterSchema
|
|
42
|
+
false: MatchConditionFilterSchema
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RelationFilterSchema(FilterSchema):
|
|
46
|
+
"""
|
|
47
|
+
Schema for configuring relation-based filters in RelationFilterViewSetMixin.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
filter_type: A tuple of (type, default_value) used to generate the query parameter
|
|
51
|
+
field in the filters schema. Example: (int, None) for optional integer filter.
|
|
52
|
+
query_param: The name of the query parameter exposed in the API endpoint.
|
|
53
|
+
This is what clients will use in requests (e.g., ?author_id=5).
|
|
54
|
+
query_filter: The Django ORM lookup to apply (e.g., "author__id", "category__slug").
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
query_filter: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MatchCaseFilterSchema(FilterSchema):
|
|
61
|
+
"""
|
|
62
|
+
Schema for configuring match-case filters in MatchCaseFilterViewSetMixin.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
filter_type: A tuple of (type, default_value) used to generate the query parameter
|
|
66
|
+
field in the filters schema. Defaults to (bool, None) for optional boolean filter.
|
|
67
|
+
query_param: The name of the query parameter exposed in the API endpoint.
|
|
68
|
+
This is what clients will use in requests (e.g., ?is_active=true).
|
|
69
|
+
cases: A BooleanMatchFilterSchema defining the filter conditions for True and False cases.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
filter_type: tuple[type, Any] = (bool, None)
|
|
73
|
+
cases: BooleanMatchFilterSchema
|
|
@@ -314,6 +314,14 @@ class APIViewSet(API):
|
|
|
314
314
|
def _check_relations_filters(self, filter: str):
|
|
315
315
|
return filter in getattr(self, "relations_filters_fields", [])
|
|
316
316
|
|
|
317
|
+
def _check_match_cases_filters(self, filter: str):
|
|
318
|
+
return filter in getattr(self, "filters_match_cases_fields", [])
|
|
319
|
+
|
|
320
|
+
def _is_special_filter(self, filter: str):
|
|
321
|
+
return self._check_relations_filters(filter) or self._check_match_cases_filters(
|
|
322
|
+
filter
|
|
323
|
+
)
|
|
324
|
+
|
|
317
325
|
def _auth_view(self, view_type: str):
|
|
318
326
|
"""
|
|
319
327
|
Resolve auth for a specific HTTP verb; falls back to self.auth if NOT_SET.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ninja_aio.views.api import APIViewSet
|
|
2
|
-
from ninja_aio.schemas import RelationFilterSchema
|
|
2
|
+
from ninja_aio.schemas import RelationFilterSchema, MatchCaseFilterSchema
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class IcontainsFilterViewSetMixin(APIViewSet):
|
|
@@ -56,7 +56,7 @@ class IcontainsFilterViewSetMixin(APIViewSet):
|
|
|
56
56
|
**{
|
|
57
57
|
f"{key}__icontains": value
|
|
58
58
|
for key, value in filters.items()
|
|
59
|
-
if isinstance(value, str) and not self.
|
|
59
|
+
if isinstance(value, str) and not self._is_special_filter(key)
|
|
60
60
|
}
|
|
61
61
|
)
|
|
62
62
|
|
|
@@ -98,7 +98,7 @@ class BooleanFilterViewSetMixin(APIViewSet):
|
|
|
98
98
|
**{
|
|
99
99
|
key: value
|
|
100
100
|
for key, value in filters.items()
|
|
101
|
-
if isinstance(value, bool) and not self.
|
|
101
|
+
if isinstance(value, bool) and not self._is_special_filter(key)
|
|
102
102
|
}
|
|
103
103
|
)
|
|
104
104
|
|
|
@@ -140,8 +140,7 @@ class NumericFilterViewSetMixin(APIViewSet):
|
|
|
140
140
|
**{
|
|
141
141
|
key: value
|
|
142
142
|
for key, value in filters.items()
|
|
143
|
-
if isinstance(value, (int, float))
|
|
144
|
-
and not self._check_relations_filters(key)
|
|
143
|
+
if isinstance(value, (int, float)) and not self._is_special_filter(key)
|
|
145
144
|
}
|
|
146
145
|
)
|
|
147
146
|
|
|
@@ -183,8 +182,7 @@ class DateFilterViewSetMixin(APIViewSet):
|
|
|
183
182
|
**{
|
|
184
183
|
f"{key}{self._compare_attr}": value
|
|
185
184
|
for key, value in filters.items()
|
|
186
|
-
if hasattr(value, "isoformat")
|
|
187
|
-
and not self._check_relations_filters(key)
|
|
185
|
+
if hasattr(value, "isoformat") and not self._is_special_filter(key)
|
|
188
186
|
}
|
|
189
187
|
)
|
|
190
188
|
|
|
@@ -346,3 +344,70 @@ class RelationFilterViewSetMixin(APIViewSet):
|
|
|
346
344
|
if value is not None:
|
|
347
345
|
rel_filters[rel_filter.query_filter] = value
|
|
348
346
|
return base_qs.filter(**rel_filters) if rel_filters else base_qs
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class MatchCaseFilterViewSetMixin(APIViewSet):
|
|
350
|
+
"""
|
|
351
|
+
Mixin providing match-case filtering for Django QuerySets.
|
|
352
|
+
This mixin applies filters based on boolean query parameters defined in
|
|
353
|
+
MatchCaseFilterSchema entries. Each entry specifies different filter conditions
|
|
354
|
+
for True and False cases.
|
|
355
|
+
Attributes:
|
|
356
|
+
filters_match_cases: List of MatchCaseFilterSchema defining the match-case filters.
|
|
357
|
+
Each schema specifies:
|
|
358
|
+
- query_param: The API query parameter name (e.g., "is_active")
|
|
359
|
+
- cases: A BooleanMatchFilterSchema with 'true' and 'false' MatchConditionFilterSchema
|
|
360
|
+
Example:
|
|
361
|
+
class UserViewSet(MatchCaseFilterViewSetMixin, APIViewSet):
|
|
362
|
+
filters_match_cases = [
|
|
363
|
+
MatchCaseFilterSchema(
|
|
364
|
+
query_param="is_active",
|
|
365
|
+
cases=BooleanMatchFilterSchema(
|
|
366
|
+
true=MatchConditionFilterSchema(
|
|
367
|
+
query_filter={"status": "active"},
|
|
368
|
+
include=True,
|
|
369
|
+
),
|
|
370
|
+
false=MatchConditionFilterSchema(
|
|
371
|
+
query_filter={"status": "inactive"},
|
|
372
|
+
include=True,
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
]
|
|
377
|
+
# GET /users?is_active=true -> queryset.filter(status="active")
|
|
378
|
+
# GET /users?is_active=false -> queryset.filter(status="inactive")
|
|
379
|
+
Notes:
|
|
380
|
+
- If the query parameter is not provided, no filtering is applied for that case.
|
|
381
|
+
- This mixin automatically registers query_params from filters_match_cases.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
filters_match_cases: list[MatchCaseFilterSchema] = []
|
|
385
|
+
|
|
386
|
+
def __init_subclass__(cls, **kwargs):
|
|
387
|
+
super().__init_subclass__(**kwargs)
|
|
388
|
+
cls.query_params = {
|
|
389
|
+
**cls.query_params,
|
|
390
|
+
**{
|
|
391
|
+
filter_match.query_param: filter_match.filter_type
|
|
392
|
+
for filter_match in cls.filters_match_cases
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def filters_match_cases_fields(self):
|
|
398
|
+
return [filter_match.query_param for filter_match in self.filters_match_cases]
|
|
399
|
+
|
|
400
|
+
async def query_params_handler(self, queryset, filters):
|
|
401
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
402
|
+
for filter_match in self.filters_match_cases:
|
|
403
|
+
value = filters.get(filter_match.query_param)
|
|
404
|
+
if value is not None:
|
|
405
|
+
case_filter = (
|
|
406
|
+
filter_match.cases.true if value else filter_match.cases.false
|
|
407
|
+
)
|
|
408
|
+
lookup = case_filter.query_filter
|
|
409
|
+
if case_filter.include:
|
|
410
|
+
base_qs = base_qs.filter(**lookup)
|
|
411
|
+
else:
|
|
412
|
+
base_qs = base_qs.exclude(**lookup)
|
|
413
|
+
return base_qs
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/core/test_renderer_parser.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
from ipaddress import IPv4Address, IPv6Address
|
|
3
3
|
|
|
4
|
+
from django.http import HttpResponse, StreamingHttpResponse
|
|
4
5
|
from django.test import TestCase, tag
|
|
5
6
|
import orjson
|
|
6
7
|
from ninja_aio.renders import ORJSONRenderer
|
|
@@ -73,3 +74,25 @@ class ORJSONRendererParserTestCase(TestCase):
|
|
|
73
74
|
rendered = self.renderer.render(DummyRequest(), int_data, response_status=200)
|
|
74
75
|
decoded = orjson.loads(rendered)
|
|
75
76
|
self.assertEqual(decoded, 42)
|
|
77
|
+
|
|
78
|
+
def test_renderer_http_response_passthrough(self):
|
|
79
|
+
"""Test that renderer returns HttpResponse as-is without serialization."""
|
|
80
|
+
response = HttpResponse(
|
|
81
|
+
b"-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----",
|
|
82
|
+
content_type="application/x-pem-file",
|
|
83
|
+
status=200,
|
|
84
|
+
)
|
|
85
|
+
rendered = self.renderer.render(DummyRequest(), response, response_status=200)
|
|
86
|
+
self.assertIs(rendered, response)
|
|
87
|
+
self.assertEqual(rendered.content, b"-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----")
|
|
88
|
+
self.assertEqual(rendered["Content-Type"], "application/x-pem-file")
|
|
89
|
+
|
|
90
|
+
def test_renderer_streaming_http_response_passthrough(self):
|
|
91
|
+
"""Test that renderer returns StreamingHttpResponse as-is."""
|
|
92
|
+
response = StreamingHttpResponse(
|
|
93
|
+
iter([b"chunk1", b"chunk2"]),
|
|
94
|
+
content_type="application/octet-stream",
|
|
95
|
+
)
|
|
96
|
+
rendered = self.renderer.render(DummyRequest(), response, response_status=200)
|
|
97
|
+
self.assertIs(rendered, response)
|
|
98
|
+
self.assertEqual(rendered["Content-Type"], "application/octet-stream")
|
|
@@ -82,6 +82,7 @@ class TestModelSerializer(BaseTestModelSerializer):
|
|
|
82
82
|
age = models.PositiveIntegerField(default=0)
|
|
83
83
|
active = models.BooleanField(default=True)
|
|
84
84
|
active_from = models.DateTimeField(auto_now_add=True)
|
|
85
|
+
status = models.CharField(max_length=20, default="pending")
|
|
85
86
|
|
|
86
87
|
|
|
87
88
|
class TestModelSerializerReverseForeignKey(BaseTestModelSerializer):
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from ninja_aio.views import mixins
|
|
3
|
-
from ninja_aio.schemas import
|
|
3
|
+
from ninja_aio.schemas import (
|
|
4
|
+
RelationFilterSchema,
|
|
5
|
+
MatchCaseFilterSchema,
|
|
6
|
+
MatchConditionFilterSchema,
|
|
7
|
+
BooleanMatchFilterSchema,
|
|
8
|
+
)
|
|
4
9
|
|
|
5
10
|
from tests.generics.views import GenericAPIViewSet
|
|
6
11
|
from tests.test_app import models, schema, serializers
|
|
@@ -178,3 +183,62 @@ class TestModelSerializerForeignKeyRelationFilterAPI(
|
|
|
178
183
|
filter_type=(str, None),
|
|
179
184
|
),
|
|
180
185
|
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ==========================================================
|
|
189
|
+
# MATCH CASE FILTER MIXIN APIS
|
|
190
|
+
# ==========================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestModelSerializerMatchCaseFilterAPI(
|
|
194
|
+
GenericAPIViewSet,
|
|
195
|
+
mixins.MatchCaseFilterViewSetMixin,
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
ViewSet for testing MatchCaseFilterViewSetMixin.
|
|
199
|
+
Uses status field to filter by 'is_approved' boolean query param.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
model = models.TestModelSerializer
|
|
203
|
+
filters_match_cases = [
|
|
204
|
+
MatchCaseFilterSchema(
|
|
205
|
+
query_param="is_approved",
|
|
206
|
+
cases=BooleanMatchFilterSchema(
|
|
207
|
+
true=MatchConditionFilterSchema(
|
|
208
|
+
query_filter={"status": "approved"},
|
|
209
|
+
include=True,
|
|
210
|
+
),
|
|
211
|
+
false=MatchConditionFilterSchema(
|
|
212
|
+
query_filter={"status": "approved"},
|
|
213
|
+
include=False,
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestModelSerializerMatchCaseExcludeFilterAPI(
|
|
221
|
+
GenericAPIViewSet,
|
|
222
|
+
mixins.MatchCaseFilterViewSetMixin,
|
|
223
|
+
):
|
|
224
|
+
"""
|
|
225
|
+
ViewSet for testing MatchCaseFilterViewSetMixin with exclude behavior.
|
|
226
|
+
Uses status field to filter by 'hide_pending' boolean query param.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
model = models.TestModelSerializer
|
|
230
|
+
filters_match_cases = [
|
|
231
|
+
MatchCaseFilterSchema(
|
|
232
|
+
query_param="hide_pending",
|
|
233
|
+
cases=BooleanMatchFilterSchema(
|
|
234
|
+
true=MatchConditionFilterSchema(
|
|
235
|
+
query_filter={"status": "pending"},
|
|
236
|
+
include=False, # exclude pending when True
|
|
237
|
+
),
|
|
238
|
+
false=MatchConditionFilterSchema(
|
|
239
|
+
query_filter={"status": "pending"},
|
|
240
|
+
include=True, # include only pending when False
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
]
|
|
@@ -971,3 +971,163 @@ class DetailSchemaFallbackTestCase(TestCase):
|
|
|
971
971
|
retrieve_schema = self.viewset._get_retrieve_schema()
|
|
972
972
|
# The retrieve schema should be the detail schema (which is the fallback)
|
|
973
973
|
self.assertEqual(retrieve_schema, self.viewset.schema_detail)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
# ==========================================================
|
|
977
|
+
# MATCH CASE FILTER MIXIN TESTS
|
|
978
|
+
# ==========================================================
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
@tag("match_case_filter_mixin")
|
|
982
|
+
class MatchCaseFilterViewSetMixinTestCase(
|
|
983
|
+
BaseTests.ModelSerializerViewSetTestCaseBase,
|
|
984
|
+
Tests.ViewSetTestCase,
|
|
985
|
+
):
|
|
986
|
+
"""Test MatchCaseFilterViewSetMixin functionality."""
|
|
987
|
+
|
|
988
|
+
namespace = "test_match_case_filter_mixin_viewset"
|
|
989
|
+
model = models.TestModelSerializer
|
|
990
|
+
viewset = views.TestModelSerializerMatchCaseFilterAPI()
|
|
991
|
+
|
|
992
|
+
async def _drop_all_objects(self):
|
|
993
|
+
await self.model.objects.all().adelete()
|
|
994
|
+
|
|
995
|
+
async def test_match_case_filter_true_includes(self):
|
|
996
|
+
"""Test filtering with True value includes matching records."""
|
|
997
|
+
await self._drop_all_objects()
|
|
998
|
+
obj_approved = await self.model.objects.acreate(
|
|
999
|
+
name="approved_item", description="desc", status="approved"
|
|
1000
|
+
)
|
|
1001
|
+
await self.model.objects.acreate(
|
|
1002
|
+
name="pending_item", description="desc", status="pending"
|
|
1003
|
+
)
|
|
1004
|
+
await self.model.objects.acreate(
|
|
1005
|
+
name="rejected_item", description="desc", status="rejected"
|
|
1006
|
+
)
|
|
1007
|
+
# Filter with is_approved=True should return only approved items
|
|
1008
|
+
res = await self.viewset.query_params_handler(
|
|
1009
|
+
self.model.objects.all(),
|
|
1010
|
+
{"is_approved": True},
|
|
1011
|
+
)
|
|
1012
|
+
self.assertEqual(await res.acount(), 1)
|
|
1013
|
+
self.assertEqual(await res.afirst(), obj_approved)
|
|
1014
|
+
|
|
1015
|
+
async def test_match_case_filter_false_excludes(self):
|
|
1016
|
+
"""Test filtering with False value excludes matching records."""
|
|
1017
|
+
await self._drop_all_objects()
|
|
1018
|
+
obj_approved = await self.model.objects.acreate(
|
|
1019
|
+
name="approved_item", description="desc", status="approved"
|
|
1020
|
+
)
|
|
1021
|
+
obj_pending = await self.model.objects.acreate(
|
|
1022
|
+
name="pending_item", description="desc", status="pending"
|
|
1023
|
+
)
|
|
1024
|
+
obj_rejected = await self.model.objects.acreate(
|
|
1025
|
+
name="rejected_item", description="desc", status="rejected"
|
|
1026
|
+
)
|
|
1027
|
+
# Filter with is_approved=False should exclude approved items
|
|
1028
|
+
res = await self.viewset.query_params_handler(
|
|
1029
|
+
self.model.objects.all(),
|
|
1030
|
+
{"is_approved": False},
|
|
1031
|
+
)
|
|
1032
|
+
self.assertEqual(await res.acount(), 2)
|
|
1033
|
+
results = [obj async for obj in res]
|
|
1034
|
+
self.assertIn(obj_pending, results)
|
|
1035
|
+
self.assertIn(obj_rejected, results)
|
|
1036
|
+
self.assertNotIn(obj_approved, results)
|
|
1037
|
+
|
|
1038
|
+
async def test_match_case_filter_none_no_filtering(self):
|
|
1039
|
+
"""Test that None filter value doesn't apply any filtering."""
|
|
1040
|
+
await self._drop_all_objects()
|
|
1041
|
+
await self.model.objects.acreate(
|
|
1042
|
+
name="approved_item", description="desc", status="approved"
|
|
1043
|
+
)
|
|
1044
|
+
await self.model.objects.acreate(
|
|
1045
|
+
name="pending_item", description="desc", status="pending"
|
|
1046
|
+
)
|
|
1047
|
+
await self.model.objects.acreate(
|
|
1048
|
+
name="rejected_item", description="desc", status="rejected"
|
|
1049
|
+
)
|
|
1050
|
+
# Filter with None should return all objects
|
|
1051
|
+
res = await self.viewset.query_params_handler(
|
|
1052
|
+
self.model.objects.all(),
|
|
1053
|
+
{"is_approved": None},
|
|
1054
|
+
)
|
|
1055
|
+
self.assertEqual(await res.acount(), 3)
|
|
1056
|
+
|
|
1057
|
+
async def test_match_case_filter_empty_filters(self):
|
|
1058
|
+
"""Test that empty filters dict returns all objects."""
|
|
1059
|
+
await self._drop_all_objects()
|
|
1060
|
+
await self.model.objects.acreate(
|
|
1061
|
+
name="approved_item", description="desc", status="approved"
|
|
1062
|
+
)
|
|
1063
|
+
await self.model.objects.acreate(
|
|
1064
|
+
name="pending_item", description="desc", status="pending"
|
|
1065
|
+
)
|
|
1066
|
+
res = await self.viewset.query_params_handler(
|
|
1067
|
+
self.model.objects.all(),
|
|
1068
|
+
{},
|
|
1069
|
+
)
|
|
1070
|
+
self.assertEqual(await res.acount(), 2)
|
|
1071
|
+
|
|
1072
|
+
def test_query_params_registered(self):
|
|
1073
|
+
"""Test that filters_match_cases are properly registered in query_params."""
|
|
1074
|
+
self.assertIn("is_approved", self.viewset.query_params)
|
|
1075
|
+
self.assertEqual(self.viewset.query_params["is_approved"], (bool, None))
|
|
1076
|
+
|
|
1077
|
+
def test_filters_match_cases_fields_property(self):
|
|
1078
|
+
"""Test that filters_match_cases_fields property returns correct list."""
|
|
1079
|
+
self.assertEqual(self.viewset.filters_match_cases_fields, ["is_approved"])
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
@tag("match_case_filter_mixin_exclude")
|
|
1083
|
+
class MatchCaseFilterViewSetMixinExcludeTestCase(TestCase):
|
|
1084
|
+
"""Test MatchCaseFilterViewSetMixin with exclude behavior."""
|
|
1085
|
+
|
|
1086
|
+
model = models.TestModelSerializer
|
|
1087
|
+
viewset = views.TestModelSerializerMatchCaseExcludeFilterAPI()
|
|
1088
|
+
|
|
1089
|
+
async def _drop_all_objects(self):
|
|
1090
|
+
await self.model.objects.all().adelete()
|
|
1091
|
+
|
|
1092
|
+
async def test_match_case_exclude_true(self):
|
|
1093
|
+
"""Test filtering with True value excludes matching records."""
|
|
1094
|
+
await self._drop_all_objects()
|
|
1095
|
+
obj_approved = await self.model.objects.acreate(
|
|
1096
|
+
name="approved_item", description="desc", status="approved"
|
|
1097
|
+
)
|
|
1098
|
+
obj_pending = await self.model.objects.acreate(
|
|
1099
|
+
name="pending_item", description="desc", status="pending"
|
|
1100
|
+
)
|
|
1101
|
+
obj_rejected = await self.model.objects.acreate(
|
|
1102
|
+
name="rejected_item", description="desc", status="rejected"
|
|
1103
|
+
)
|
|
1104
|
+
# Filter with hide_pending=True should exclude pending items
|
|
1105
|
+
res = await self.viewset.query_params_handler(
|
|
1106
|
+
self.model.objects.all(),
|
|
1107
|
+
{"hide_pending": True},
|
|
1108
|
+
)
|
|
1109
|
+
self.assertEqual(await res.acount(), 2)
|
|
1110
|
+
results = [obj async for obj in res]
|
|
1111
|
+
self.assertIn(obj_approved, results)
|
|
1112
|
+
self.assertIn(obj_rejected, results)
|
|
1113
|
+
self.assertNotIn(obj_pending, results)
|
|
1114
|
+
|
|
1115
|
+
async def test_match_case_exclude_false_includes_only(self):
|
|
1116
|
+
"""Test filtering with False value includes only matching records."""
|
|
1117
|
+
await self._drop_all_objects()
|
|
1118
|
+
await self.model.objects.acreate(
|
|
1119
|
+
name="approved_item", description="desc", status="approved"
|
|
1120
|
+
)
|
|
1121
|
+
obj_pending = await self.model.objects.acreate(
|
|
1122
|
+
name="pending_item", description="desc", status="pending"
|
|
1123
|
+
)
|
|
1124
|
+
await self.model.objects.acreate(
|
|
1125
|
+
name="rejected_item", description="desc", status="rejected"
|
|
1126
|
+
)
|
|
1127
|
+
# Filter with hide_pending=False should include only pending items
|
|
1128
|
+
res = await self.viewset.query_params_handler(
|
|
1129
|
+
self.model.objects.all(),
|
|
1130
|
+
{"hide_pending": False},
|
|
1131
|
+
)
|
|
1132
|
+
self.assertEqual(await res.acount(), 1)
|
|
1133
|
+
self.assertEqual(await res.afirst(), obj_pending)
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# ORJSON renderer option
|
|
2
|
-
|
|
3
|
-
This package uses an internal ORJSON-based renderer. Configure serialization options via Django settings:
|
|
4
|
-
|
|
5
|
-
```python
|
|
6
|
-
# settings.py
|
|
7
|
-
import orjson
|
|
8
|
-
|
|
9
|
-
# Single option
|
|
10
|
-
NINJA_AIO_ORJSON_RENDERER_OPTION = orjson.OPT_INDENT_2
|
|
11
|
-
|
|
12
|
-
# Multiple options (bitwise OR)
|
|
13
|
-
NINJA_AIO_ORJSON_RENDERER_OPTION = (
|
|
14
|
-
orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
|
|
15
|
-
)
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Notes:
|
|
19
|
-
- The value is an orjson option bitmask (e.g., `orjson.OPT_INDENT_2`, `orjson.OPT_NON_STR_KEYS`), and you can combine multiple options using `|`.
|
|
20
|
-
- If not set, the default `orjson.dumps` options are used.
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.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.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/models/model_serializer.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/models/serializers.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/api/views/api_view_set.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
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/installation.md
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/docs/getting_started/quick_start.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.10.1 → django_ninja_aio_crud-2.11.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
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/decorators/__init__.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/decorators/operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/factory/operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/ninja_aio/models/serializers.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
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/core/test_exceptions_api.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.10.1 → django_ninja_aio_crud-2.11.1}/tests/helpers/test_many_to_many_api.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.1}/tests/models/test_model_util.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.10.1 → django_ninja_aio_crud-2.11.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
|