vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.32k stars 436 forks source link

[BUG] ModelSchema with OneToOneField relation error #350

Open gabrielfgularte opened 2 years ago

gabrielfgularte commented 2 years ago

Describe the bug Models that are OneToOne related are producing an RelatedObjectDoesNotExist error using ModelSchema.

# models.py
class User(models.Model):
    email = models.EmailField(unique=True)

class Account(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    username = models.CharField(max_length=15)

    @property
    def full_repr(self):
        return f'{self.username} <{self.user.email}>'

# schemas.py
class AccountSchema(ModelSchema):
    full_repr: str = None

    class Config:
        model = Account
        model_exclude = ['id', 'user']

class UserSchema(ModelSchema):
    account: AccountSchema = None

    class Config:
        model = User

# routes.py
class AuthBearer(HttpBearer):
    def authenticate(self, request, token):
        token = get_session(key)
        if token:
            return get_object_or_404(User, pk=token.user_id, is_active=True)

@router.get('/', auth=AuthBearer(), response=UserSchema)
def user_detail(request):
    return request.user

This code produces the following exceptions:

Unauthorized: /api/accounts/sessions/
'User' object has no attribute 'template'
Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 54, in __getitem__
    item = getattr(self._obj, key)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 421, in __get__
    raise self.RelatedObjectDoesNotExist(
accounts.models.user.User.account.RelatedObjectDoesNotExist: User has no account.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 862, in _resolve_lookup
    current = current[bit]
TypeError: 'User' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 870, in _resolve_lookup
    current = getattr(current, bit)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 421, in __get__
    raise self.RelatedObjectDoesNotExist(
accounts.models.user.User.account.RelatedObjectDoesNotExist: User has no account.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/operation.py", line 100, in run
    return self._result_to_response(request, result)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/operation.py", line 193, in _result_to_response
    result = response_model.from_orm(resp_object).dict(
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 161, in from_orm
    return super().from_orm(obj)
  File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic/main.py", line 1022, in pydantic.main.validate_model
  File "pydantic/fields.py", line 854, in pydantic.fields.ModelField.validate
  File "pydantic/fields.py", line 1071, in pydantic.fields.ModelField._validate_singleton
  File "pydantic/fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
  File "pydantic/class_validators.py", line 313, in pydantic.class_validators._generic_validator_basic.lambda12
  File "pydantic/main.py", line 678, in pydantic.main.BaseModel.validate
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 161, in from_orm
    return super().from_orm(obj)
  File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic/main.py", line 1001, in pydantic.main.validate_model
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 67, in get
    return self[key]
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 58, in __getitem__
    item = Variable(key).resolve(self._obj)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 829, in resolve
    value = self._resolve_lookup(context)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 910, in _resolve_lookup
    current = context.template.engine.string_if_invalid
AttributeError: 'User' object has no attribute 'template'
Internal Server Error: /api/accounts/sessions/
'User' object has no attribute 'template'
Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 54, in __getitem__
    item = getattr(self._obj, key)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 421, in __get__
    raise self.RelatedObjectDoesNotExist(
accounts.models.user.User.account.RelatedObjectDoesNotExist: User has no account.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 862, in _resolve_lookup
    current = current[bit]
TypeError: 'User' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 870, in _resolve_lookup
    current = getattr(current, bit)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 421, in __get__
    raise self.RelatedObjectDoesNotExist(
accounts.models.user.User.account.RelatedObjectDoesNotExist: User has no account.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/operation.py", line 100, in run
    return self._result_to_response(request, result)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/operation.py", line 193, in _result_to_response
    result = response_model.from_orm(resp_object).dict(
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 161, in from_orm
    return super().from_orm(obj)
  File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic/main.py", line 1022, in pydantic.main.validate_model
  File "pydantic/fields.py", line 854, in pydantic.fields.ModelField.validate
  File "pydantic/fields.py", line 1071, in pydantic.fields.ModelField._validate_singleton
  File "pydantic/fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
  File "pydantic/class_validators.py", line 313, in pydantic.class_validators._generic_validator_basic.lambda12
  File "pydantic/main.py", line 678, in pydantic.main.BaseModel.validate
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 161, in from_orm
    return super().from_orm(obj)
  File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic/main.py", line 1001, in pydantic.main.validate_model
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 67, in get
    return self[key]
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/ninja/schema.py", line 58, in __getitem__
    item = Variable(key).resolve(self._obj)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 829, in resolve
    value = self._resolve_lookup(context)
  File "/root/.local/share/virtualenvs/app-4PlAip0Q/lib/python3.8/site-packages/django/template/base.py", line 910, in _resolve_lookup
    current = context.template.engine.string_if_invalid
AttributeError: 'User' object has no attribute 'template'
Internal Server Error: /api/accounts/sessions/

Versions (please complete the following information):

vitalik commented 2 years ago

as quick glance I see that you have

user as required (null=False) on Account

    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

but in schema it accepts None:

    account: AccountSchema = None

also Account was not created for the user - that is general source of the error

I guess you need to have some code that automatically creates account for each created user or something (or use null=True)

gabrielfgularte commented 2 years ago

Got you. My project assumes that a user can live without an account. So an account can be created later. I'll try with null=True. Thank you very much!

gabrielfgularte commented 2 years ago

@vitalik Same issue with null=True in the user field =/

gabrielfgularte commented 2 years ago

I have a workaround. If I put a resolver on the Schema using hasattr it works:

class UserSchema(ModelSchema):
    account: Optional[AccountSchema] = None

    class Config:
        model = User

    @staticmethod
    def resolve_account(obj):
        if hasattr(obj, 'account'):
            return obj.account
        return None
vitalik commented 2 years ago

yeah, I guess this is the best solution for now for OneToOneField case

rkulinski commented 1 year ago

Are there any plans to tackle this?

vitalik commented 1 year ago

Are there any plans to tackle this?

@rkulinski the resolve_ seems working good here ? what's your vision ?

maybe this also can work


class User(models.Model):
    email = models.EmailField(unique=True)

    def get_account(self):
         if hasattr(obj, 'account'):
            return obj.account

...

class UserSchema(ModelSchema):
    account: Optional[AccountSchema] = Field(None, alias='get_account')

the problem with automating this is that we cannot be sure of developer mind here if onetoone relations must be enforced or optional at runtime

rkulinski commented 1 year ago

That's how we work around this.