django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

[BUG] ModelSchema with OneToOneField relation error

Open gabrielfgularte opened this issue 3 years ago • 5 comments

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):

  • Python version: 3.8.12
  • Django version: 4.0.2
  • Django-Ninja version: 0.17.0

gabrielfgularte avatar Feb 07 '22 18:02 gabrielfgularte

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)

vitalik avatar Feb 07 '22 19:02 vitalik

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 avatar Feb 07 '22 19:02 gabrielfgularte

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

gabrielfgularte avatar Feb 07 '22 19:02 gabrielfgularte

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

gabrielfgularte avatar Feb 08 '22 16:02 gabrielfgularte

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

vitalik avatar Feb 08 '22 16:02 vitalik

Are there any plans to tackle this?

rkulinski avatar Aug 21 '23 12:08 rkulinski

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

vitalik avatar Aug 22 '23 14:08 vitalik

That's how we work around this.

rkulinski avatar Sep 15 '23 14:09 rkulinski