django-ninja-aio-crud 2.14.0__tar.gz → 2.15.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.14.0 → django_ninja_aio_crud-2.15.1}/.github/workflows/docs.yml +2 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/model_serializer.md +58 -6
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/serializers.md +42 -6
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/auth.py +0 -5
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/serializers.py +30 -13
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/models.py +178 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_auth.py +2 -3
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_serializers.py +440 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/views/test_viewset.py +1 -1
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.gitignore +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/LICENSE +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/README.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/api_view_set.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/main.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/filters.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/views/test_views.py +0 -0
|
@@ -27,6 +27,7 @@ on:
|
|
|
27
27
|
- "2.12"
|
|
28
28
|
- "2.13"
|
|
29
29
|
- "2.14"
|
|
30
|
+
- "2.15"
|
|
30
31
|
make_latest:
|
|
31
32
|
description: 'Set as "latest" and default?'
|
|
32
33
|
type: boolean
|
|
@@ -57,6 +58,7 @@ on:
|
|
|
57
58
|
- "2.12"
|
|
58
59
|
- "2.13"
|
|
59
60
|
- "2.14"
|
|
61
|
+
- "2.15"
|
|
60
62
|
delete_confirm:
|
|
61
63
|
description: 'Confirm deletion of the selected version'
|
|
62
64
|
type: boolean
|
{django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/model_serializer.md
RENAMED
|
@@ -419,12 +419,14 @@ Use `relations_as_id` to serialize relation fields as IDs instead of nested obje
|
|
|
419
419
|
|
|
420
420
|
| Relation Type | Output Type | Example Value |
|
|
421
421
|
|--------------------|-------------------|----------------------|
|
|
422
|
-
| Forward FK | `
|
|
423
|
-
| Forward O2O | `
|
|
424
|
-
| Reverse FK | `list[
|
|
425
|
-
| Reverse O2O | `
|
|
426
|
-
| M2M (forward) | `list[
|
|
427
|
-
| M2M (reverse) | `list[
|
|
422
|
+
| Forward FK | `PK_TYPE \| None` | `5` or `null` |
|
|
423
|
+
| Forward O2O | `PK_TYPE \| None` | `3` or `null` |
|
|
424
|
+
| Reverse FK | `list[PK_TYPE]` | `[1, 2, 3]` |
|
|
425
|
+
| Reverse O2O | `PK_TYPE \| None` | `7` or `null` |
|
|
426
|
+
| M2M (forward) | `list[PK_TYPE]` | `[1, 2]` |
|
|
427
|
+
| M2M (reverse) | `list[PK_TYPE]` | `[4, 5, 6]` |
|
|
428
|
+
|
|
429
|
+
**Note:** `PK_TYPE` is automatically detected from the related model's primary key field. Supported types include `int` (default), `UUID`, `str`, and any other Django primary key type.
|
|
428
430
|
|
|
429
431
|
**Example:**
|
|
430
432
|
|
|
@@ -494,6 +496,56 @@ class Article(ModelSerializer):
|
|
|
494
496
|
}
|
|
495
497
|
```
|
|
496
498
|
|
|
499
|
+
**UUID Primary Key Example:**
|
|
500
|
+
|
|
501
|
+
When models use UUID primary keys, the output type is automatically `UUID`:
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
import uuid
|
|
505
|
+
from django.db import models
|
|
506
|
+
from ninja_aio.models import ModelSerializer
|
|
507
|
+
|
|
508
|
+
class Author(ModelSerializer):
|
|
509
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
510
|
+
name = models.CharField(max_length=200)
|
|
511
|
+
|
|
512
|
+
class ReadSerializer:
|
|
513
|
+
fields = ["id", "name", "books"]
|
|
514
|
+
relations_as_id = ["books"]
|
|
515
|
+
|
|
516
|
+
class Book(ModelSerializer):
|
|
517
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
518
|
+
title = models.CharField(max_length=200)
|
|
519
|
+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
|
|
520
|
+
|
|
521
|
+
class ReadSerializer:
|
|
522
|
+
fields = ["id", "title", "author"]
|
|
523
|
+
relations_as_id = ["author"]
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Output (Author with UUID):**
|
|
527
|
+
|
|
528
|
+
```json
|
|
529
|
+
{
|
|
530
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
531
|
+
"name": "J.K. Rowling",
|
|
532
|
+
"books": [
|
|
533
|
+
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
534
|
+
"6ba7b811-9dad-11d1-80b4-00c04fd430c8"
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Output (Book with UUID):**
|
|
540
|
+
|
|
541
|
+
```json
|
|
542
|
+
{
|
|
543
|
+
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
544
|
+
"title": "Harry Potter",
|
|
545
|
+
"author": "550e8400-e29b-41d4-a716-446655440000"
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
497
549
|
**Query Optimization Note:** When using `relations_as_id`, you should still use `select_related()` for forward relations and `prefetch_related()` for reverse/M2M relations to avoid N+1 queries:
|
|
498
550
|
|
|
499
551
|
```python
|
{django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/serializers.md
RENAMED
|
@@ -429,12 +429,14 @@ Use `relations_as_id` in Meta to serialize relation fields as IDs instead of nes
|
|
|
429
429
|
|
|
430
430
|
| Relation Type | Output Type | Example Value |
|
|
431
431
|
|--------------------|-------------------|----------------------|
|
|
432
|
-
| Forward FK | `
|
|
433
|
-
| Forward O2O | `
|
|
434
|
-
| Reverse FK | `list[
|
|
435
|
-
| Reverse O2O | `
|
|
436
|
-
| M2M (forward) | `list[
|
|
437
|
-
| M2M (reverse) | `list[
|
|
432
|
+
| Forward FK | `PK_TYPE \| None` | `5` or `null` |
|
|
433
|
+
| Forward O2O | `PK_TYPE \| None` | `3` or `null` |
|
|
434
|
+
| Reverse FK | `list[PK_TYPE]` | `[1, 2, 3]` |
|
|
435
|
+
| Reverse O2O | `PK_TYPE \| None` | `7` or `null` |
|
|
436
|
+
| M2M (forward) | `list[PK_TYPE]` | `[1, 2]` |
|
|
437
|
+
| M2M (reverse) | `list[PK_TYPE]` | `[4, 5, 6]` |
|
|
438
|
+
|
|
439
|
+
**Note:** `PK_TYPE` is automatically detected from the related model's primary key field. Supported types include `int` (default), `UUID`, `str`, and any other Django primary key type.
|
|
438
440
|
|
|
439
441
|
**Example:**
|
|
440
442
|
|
|
@@ -509,6 +511,40 @@ class ArticleSerializer(serializers.Serializer):
|
|
|
509
511
|
}
|
|
510
512
|
```
|
|
511
513
|
|
|
514
|
+
**UUID Primary Key Example:**
|
|
515
|
+
|
|
516
|
+
When related models use UUID primary keys, the output type is automatically `UUID`:
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
import uuid
|
|
520
|
+
from django.db import models
|
|
521
|
+
|
|
522
|
+
class Author(models.Model):
|
|
523
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
524
|
+
name = models.CharField(max_length=200)
|
|
525
|
+
|
|
526
|
+
class AuthorSerializer(serializers.Serializer):
|
|
527
|
+
class Meta:
|
|
528
|
+
model = Author
|
|
529
|
+
schema_out = serializers.SchemaModelConfig(
|
|
530
|
+
fields=["id", "name", "books"]
|
|
531
|
+
)
|
|
532
|
+
relations_as_id = ["books"]
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Output (Author with UUID):**
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{
|
|
539
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
540
|
+
"name": "J.K. Rowling",
|
|
541
|
+
"books": [
|
|
542
|
+
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
543
|
+
"6ba7b811-9dad-11d1-80b4-00c04fd430c8"
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
512
548
|
**Combining with relations_serializers:**
|
|
513
549
|
|
|
514
550
|
You can use both `relations_as_id` and `relations_serializers` in the same serializer. Fields in `relations_as_id` take precedence:
|
|
@@ -107,11 +107,6 @@ class AsyncJwtBearer(HttpBearer):
|
|
|
107
107
|
"""
|
|
108
108
|
try:
|
|
109
109
|
self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
|
|
110
|
-
except ValueError:
|
|
111
|
-
# raise AuthError(", ".join(exc.args), 401)
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
110
|
self.validate_claims(self.dcd.claims)
|
|
116
111
|
except errors.JoseError:
|
|
117
112
|
return False
|
{django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/serializers.py
RENAMED
|
@@ -46,8 +46,23 @@ def _extract_pk(v: Any) -> Any:
|
|
|
46
46
|
return v
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
class PkFromModel:
|
|
50
|
+
"""Subscriptable type for extracting PK from model instances during serialization.
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
PkFromModel[int] -> for integer PKs
|
|
54
|
+
PkFromModel[str] -> for string PKs
|
|
55
|
+
PkFromModel[UUID] -> for UUID PKs
|
|
56
|
+
PkFromModel -> defaults to int (backwards compatible)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
_default = Annotated[int, BeforeValidator(_extract_pk)]
|
|
60
|
+
|
|
61
|
+
def __class_getitem__(cls, pk_type: type) -> type:
|
|
62
|
+
return Annotated[pk_type, BeforeValidator(_extract_pk)]
|
|
63
|
+
|
|
64
|
+
def __new__(cls):
|
|
65
|
+
return cls._default
|
|
51
66
|
|
|
52
67
|
|
|
53
68
|
class BaseSerializer:
|
|
@@ -290,9 +305,8 @@ class BaseSerializer:
|
|
|
290
305
|
"""
|
|
291
306
|
# Auto-resolve ModelSerializer with readable fields
|
|
292
307
|
if isinstance(rel_model, ModelSerializerMeta):
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return None
|
|
308
|
+
has_readable_fields = rel_model.get_fields("read") or rel_model.get_custom_fields("read")
|
|
309
|
+
return rel_model.generate_related_s() if has_readable_fields else None
|
|
296
310
|
|
|
297
311
|
# Resolve from explicit serializer mapping
|
|
298
312
|
rel_serializers = cls._get_relations_serializers() or {}
|
|
@@ -415,12 +429,14 @@ class BaseSerializer:
|
|
|
415
429
|
|
|
416
430
|
# Handle relations_as_id for reverse relations
|
|
417
431
|
if field_name in relations_as_id:
|
|
432
|
+
from ninja_aio.models.utils import ModelUtil
|
|
433
|
+
pk_field_type = ModelUtil(rel_model).pk_field_type
|
|
418
434
|
if many:
|
|
419
435
|
# For many relations, use PkFromModel to extract PKs from model instances
|
|
420
|
-
return (field_name, list[PkFromModel], Field(default_factory=list))
|
|
436
|
+
return (field_name, list[PkFromModel[pk_field_type]], Field(default_factory=list))
|
|
421
437
|
else:
|
|
422
438
|
# For single reverse relations (ReverseOneToOne), extract pk
|
|
423
|
-
return (field_name, PkFromModel | None, None)
|
|
439
|
+
return (field_name, PkFromModel[pk_field_type] | None, None)
|
|
424
440
|
|
|
425
441
|
schema = cls._resolve_relation_schema(field_name, rel_model)
|
|
426
442
|
if not schema:
|
|
@@ -455,15 +471,16 @@ class BaseSerializer:
|
|
|
455
471
|
|
|
456
472
|
# Handle relations_as_id: serialize as the raw FK ID
|
|
457
473
|
if field_name in relations_as_id:
|
|
474
|
+
from ninja_aio.models.utils import ModelUtil
|
|
475
|
+
pk_field_type = ModelUtil(rel_model).pk_field_type
|
|
458
476
|
# Use PkFromModel to extract pk from the related instance during serialization
|
|
459
|
-
return (field_name, PkFromModel | None, None)
|
|
477
|
+
return (field_name, PkFromModel[pk_field_type] | None, None)
|
|
460
478
|
|
|
461
479
|
# Special case: ModelSerializer with no readable fields should be skipped entirely
|
|
462
|
-
if isinstance(rel_model, ModelSerializerMeta)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
return None
|
|
480
|
+
if isinstance(rel_model, ModelSerializerMeta) and not (
|
|
481
|
+
rel_model.get_fields("read") or rel_model.get_custom_fields("read")
|
|
482
|
+
):
|
|
483
|
+
return None
|
|
467
484
|
|
|
468
485
|
schema = cls._resolve_relation_schema(field_name, rel_model)
|
|
469
486
|
if not schema:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
1
3
|
from ninja_aio.models import ModelSerializer
|
|
2
4
|
from django.db import models
|
|
3
5
|
|
|
@@ -298,3 +300,179 @@ class ArticleAsId(BaseTestModelSerializer):
|
|
|
298
300
|
class ReadSerializer:
|
|
299
301
|
fields = ["id", "name", "description", "tags_as_id"]
|
|
300
302
|
relations_as_id = ["tags_as_id"]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ==========================================================
|
|
306
|
+
# RELATIONS AS ID WITH UUID PK TEST MODELS
|
|
307
|
+
# ==========================================================
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class BaseUUIDTestModel(ModelSerializer):
|
|
311
|
+
"""Base model with UUID primary key."""
|
|
312
|
+
|
|
313
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
314
|
+
name = models.CharField(max_length=255)
|
|
315
|
+
|
|
316
|
+
class Meta:
|
|
317
|
+
abstract = True
|
|
318
|
+
|
|
319
|
+
class ReadSerializer:
|
|
320
|
+
fields = ["id", "name"]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class AuthorUUID(BaseUUIDTestModel):
|
|
324
|
+
"""Author model with UUID PK for testing relations_as_id on reverse FK."""
|
|
325
|
+
|
|
326
|
+
class ReadSerializer:
|
|
327
|
+
fields = ["id", "name", "books_uuid"]
|
|
328
|
+
relations_as_id = ["books_uuid"]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class BookUUID(BaseUUIDTestModel):
|
|
332
|
+
"""Book model with UUID PK for testing relations_as_id on forward FK."""
|
|
333
|
+
|
|
334
|
+
author_uuid = models.ForeignKey(
|
|
335
|
+
AuthorUUID,
|
|
336
|
+
on_delete=models.CASCADE,
|
|
337
|
+
related_name="books_uuid",
|
|
338
|
+
null=True,
|
|
339
|
+
blank=True,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
class ReadSerializer:
|
|
343
|
+
fields = ["id", "name", "author_uuid"]
|
|
344
|
+
relations_as_id = ["author_uuid"]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class ProfileUUID(BaseUUIDTestModel):
|
|
348
|
+
"""Profile model with UUID PK for testing relations_as_id on reverse O2O."""
|
|
349
|
+
|
|
350
|
+
class ReadSerializer:
|
|
351
|
+
fields = ["id", "name", "user_uuid"]
|
|
352
|
+
relations_as_id = ["user_uuid"]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class UserUUID(BaseUUIDTestModel):
|
|
356
|
+
"""User model with UUID PK for testing relations_as_id on forward O2O."""
|
|
357
|
+
|
|
358
|
+
profile_uuid = models.OneToOneField(
|
|
359
|
+
ProfileUUID,
|
|
360
|
+
on_delete=models.CASCADE,
|
|
361
|
+
related_name="user_uuid",
|
|
362
|
+
null=True,
|
|
363
|
+
blank=True,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
class ReadSerializer:
|
|
367
|
+
fields = ["id", "name", "profile_uuid"]
|
|
368
|
+
relations_as_id = ["profile_uuid"]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class TagUUID(BaseUUIDTestModel):
|
|
372
|
+
"""Tag model with UUID PK for testing relations_as_id on reverse M2M."""
|
|
373
|
+
|
|
374
|
+
class ReadSerializer:
|
|
375
|
+
fields = ["id", "name", "articles_uuid"]
|
|
376
|
+
relations_as_id = ["articles_uuid"]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class ArticleUUID(BaseUUIDTestModel):
|
|
380
|
+
"""Article model with UUID PK for testing relations_as_id on forward M2M."""
|
|
381
|
+
|
|
382
|
+
tags_uuid = models.ManyToManyField(
|
|
383
|
+
TagUUID,
|
|
384
|
+
related_name="articles_uuid",
|
|
385
|
+
blank=True,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
class ReadSerializer:
|
|
389
|
+
fields = ["id", "name", "tags_uuid"]
|
|
390
|
+
relations_as_id = ["tags_uuid"]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ==========================================================
|
|
394
|
+
# RELATIONS AS ID WITH STRING PK TEST MODELS
|
|
395
|
+
# ==========================================================
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class BaseStringPKTestModel(ModelSerializer):
|
|
399
|
+
"""Base model with string (CharField) primary key."""
|
|
400
|
+
|
|
401
|
+
id = models.CharField(primary_key=True, max_length=50)
|
|
402
|
+
name = models.CharField(max_length=255)
|
|
403
|
+
|
|
404
|
+
class Meta:
|
|
405
|
+
abstract = True
|
|
406
|
+
|
|
407
|
+
class ReadSerializer:
|
|
408
|
+
fields = ["id", "name"]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class AuthorStringPK(BaseStringPKTestModel):
|
|
412
|
+
"""Author model with string PK for testing relations_as_id on reverse FK."""
|
|
413
|
+
|
|
414
|
+
class ReadSerializer:
|
|
415
|
+
fields = ["id", "name", "books_str"]
|
|
416
|
+
relations_as_id = ["books_str"]
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class BookStringPK(BaseStringPKTestModel):
|
|
420
|
+
"""Book model with string PK for testing relations_as_id on forward FK."""
|
|
421
|
+
|
|
422
|
+
author_str = models.ForeignKey(
|
|
423
|
+
AuthorStringPK,
|
|
424
|
+
on_delete=models.CASCADE,
|
|
425
|
+
related_name="books_str",
|
|
426
|
+
null=True,
|
|
427
|
+
blank=True,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
class ReadSerializer:
|
|
431
|
+
fields = ["id", "name", "author_str"]
|
|
432
|
+
relations_as_id = ["author_str"]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class ProfileStringPK(BaseStringPKTestModel):
|
|
436
|
+
"""Profile model with string PK for testing relations_as_id on reverse O2O."""
|
|
437
|
+
|
|
438
|
+
class ReadSerializer:
|
|
439
|
+
fields = ["id", "name", "user_str"]
|
|
440
|
+
relations_as_id = ["user_str"]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class UserStringPK(BaseStringPKTestModel):
|
|
444
|
+
"""User model with string PK for testing relations_as_id on forward O2O."""
|
|
445
|
+
|
|
446
|
+
profile_str = models.OneToOneField(
|
|
447
|
+
ProfileStringPK,
|
|
448
|
+
on_delete=models.CASCADE,
|
|
449
|
+
related_name="user_str",
|
|
450
|
+
null=True,
|
|
451
|
+
blank=True,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
class ReadSerializer:
|
|
455
|
+
fields = ["id", "name", "profile_str"]
|
|
456
|
+
relations_as_id = ["profile_str"]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class TagStringPK(BaseStringPKTestModel):
|
|
460
|
+
"""Tag model with string PK for testing relations_as_id on reverse M2M."""
|
|
461
|
+
|
|
462
|
+
class ReadSerializer:
|
|
463
|
+
fields = ["id", "name", "articles_str"]
|
|
464
|
+
relations_as_id = ["articles_str"]
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class ArticleStringPK(BaseStringPKTestModel):
|
|
468
|
+
"""Article model with string PK for testing relations_as_id on forward M2M."""
|
|
469
|
+
|
|
470
|
+
tags_str = models.ManyToManyField(
|
|
471
|
+
TagStringPK,
|
|
472
|
+
related_name="articles_str",
|
|
473
|
+
blank=True,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
class ReadSerializer:
|
|
477
|
+
fields = ["id", "name", "tags_str"]
|
|
478
|
+
relations_as_id = ["tags_str"]
|
|
@@ -138,12 +138,11 @@ class JwtAuthTests(TestCase):
|
|
|
138
138
|
return "should-not-happen"
|
|
139
139
|
|
|
140
140
|
bearer = TB()
|
|
141
|
-
|
|
142
|
-
# Mock jwt.decode to raise ValueError to test that branch
|
|
141
|
+
|
|
143
142
|
import unittest.mock as mock
|
|
144
143
|
|
|
145
144
|
with mock.patch("ninja_aio.auth.jwt.decode") as mock_decode:
|
|
146
|
-
mock_decode.side_effect =
|
|
145
|
+
mock_decode.side_effect = errors.JoseError("invalid token")
|
|
147
146
|
result = async_to_sync(bearer.authenticate)(HttpRequest(), "fake-token")
|
|
148
147
|
self.assertFalse(result)
|
|
149
148
|
|