Skip to content

Commit

Permalink
OneToOne and ManyToMany models are nullable (#8)
Browse files Browse the repository at this point in the history
These foreign key relationships should be nullable and as such this
should be reflected in the schema and when making a model instance into
a schema.

Prior to this this would throw an error, as it was not recognized as
nullable.
  • Loading branch information
jonathan-s authored May 26, 2024
1 parent 999984f commit c1e68e3
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 14 deletions.
38 changes: 26 additions & 12 deletions djantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from enum import Enum
from typing import Any, Dict, List, Union
from typing import Any, Dict, List, Union, Optional
import typing
from uuid import UUID

from django.utils.functional import Promise
Expand Down Expand Up @@ -130,9 +131,6 @@ def ModelSchemaField(field: Any, schema_name: str) -> tuple:
if blank or null:
python_type = Union[python_type, None]

if field.related_model:
field = field.target_field

else:
if field.choices:
enum_choices = {}
Expand Down Expand Up @@ -182,21 +180,37 @@ def ModelSchemaField(field: Any, schema_name: str) -> tuple:
title = field.verbose_name.title()

if not description:
description = field.name
related_field_name = None
if field.related_model:
related_field_name = field.target_field.name

description = related_field_name or field.name

if (
getattr(field, "get_internal_type", lambda: None)() in STR_TYPES
and not field.choices
):
max_length = field.max_length

field_info = FieldInfo(
default=default,
default_factory=default_factory,
title=title,
description=str(description),
max_length=max_length,
)

field_is_optional = all([
getattr(field, "null", None),
field.is_relation,
# A list that is null, is the empty list. So there is no need
# to make it nullable.
typing.get_origin(python_type) is not list
])
if field_is_optional:
python_type = Optional[python_type]

return (
python_type,
FieldInfo(
default=default,
default_factory=default_factory,
title=title,
description=str(description),
max_length=max_length,
),
field_info
)
30 changes: 29 additions & 1 deletion tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
import pytest
from pydantic import ConfigDict
from testapp.models import Configuration, Listing, Preference, Record, Searchable
from testapp.models import Configuration, Listing, Preference, Record, Searchable, User

from djantic import ModelSchema


@pytest.mark.django_db
def test_schema_without_include_and_exclude():
"""
Using a schema without include and exclude populates userschema correctly
Optional foreign key handled gracefully.
"""

user = User.objects.create(
first_name="Jordan", last_name="Eremieff", email="[email protected]"
)

class UserSchema(ModelSchema):
model_config = ConfigDict(model=User)

dumped = UserSchema.from_django(user).model_dump()
# These values are here, but a hassle to use freeze gun.
dumped.pop("updated_at")
dumped.pop("created_at")

assert dumped == {
"first_name": "Jordan",
"id": 1,
"email": "[email protected]",
"profile": None,
"last_name": "Eremieff",
}


@pytest.mark.django_db
def test_unhandled_field_type():
class SearchableSchema(ModelSchema):
Expand Down
9 changes: 8 additions & 1 deletion tests/test_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,17 @@ class ProfileWithUserSchema(ModelSchema):
"description": "A user of the application.",
"properties": {
"profile": {
"anyOf": [
{
"type": "integer",
},
{
"type": "null",
},
],
"default": None,
"description": "id",
"title": "Profile",
"type": "integer",
},
"id": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
Expand Down

0 comments on commit c1e68e3

Please sign in to comment.