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

Using AutoOneToOneField with proxy model

Open johncpang opened this issue 6 years ago • 2 comments

When using AutoOneToOneField with Proxy model, I've a hard time to get the related model. The similar setup with OneToOneField doesn't have problem. While I check the code I found a difference here:

class AutoOneToOneField(OneToOneField):
    def contribute_to_related_class(self, cls, related):
        setattr(
            cls,
            related.get_accessor_name(),
            AutoSingleRelatedObjectDescriptor(related)
        )

Studying Django's code:

class ForeignObject(RelatedField):
    def contribute_to_related_class(self, cls, related):
        if not self.remote_field.is_hidden() and not related.related_model._meta.swapped:
            setattr(cls._meta.concrete_model, related.get_accessor_name(), self.related_accessor_class(related))
            if self.remote_field.limit_choices_to:
                cls._meta.related_fkey_lookups.append(self.remote_field.limit_choices_to)

When calling setattr(...), should AutoOneToOneField also pass in cls._meta.concrete_model instead?

johncpang avatar Sep 12 '18 07:09 johncpang

Just verified that cls should really be cls._meta.concrete_model.

Here is my project which use a proxy model of Auth.User to filter out staff and superuser, also to display by email instead of username. Since I use/mix with Firebase SDK, username is Firebase's UID and hidden from end user. My admin stuff can only recognize end users by their email addresses.

from django.contrib.auth.models import UserManager, User

class EndUserManager(UserManager):
	def get_queryset(self):
		return super().get_queryset().filter(is_staff=False, is_superuser=False)

class EndUser(User):
	objects = EndUserManager()

	class Meta:
		proxy = True

	def __str__(self):
		return "%s" % (self.email or self.username)

Now I've two models which both are One-To-One relation with EndUser, and display with email obtained from EndUser.__str__():

class Profile(models.Model):
	owner = AutoOneToOneField(EndUser, on_delete=models.CASCADE, related_name='profile')

	def __str__(self):
		return "%s" % self.owner

class Tutor(models.Model):
	owner = OneToOneField(EndUser, on_delete=models.CASCADE, related_name='tutor')

	def __str__(self):
		return "%s" % self.owner

When I obtain an instance of User and try to access .profile and .tutor, I got error for profile but no error for tutor. Here is my test done in python shell:

>>> user = User.objects.get(pk=100)
>>> user.profile
AttributeError: 'User' object has no attribute 'profile'
>>> user.tutor
<Tutor: [email protected]>

I know I can obtain instance of EndUser by a database call. Since I want to avoid that, I force User to become EndUser (such as self.request.user.__class__ = EndUser). At first it seems working, at least I didn't see any errors, but with AutoOneToOneField, it doesn't.

Using a patched version AutoOneToOneField does resolve the problem.

class FixedAutoOneToOneField(AutoOneToOneField):

	def contribute_to_related_class(self, cls, related):
		setattr(
			cls._meta.concrete_model,
			related.get_accessor_name(),
			AutoSingleRelatedObjectDescriptor(related)
		)

johncpang avatar Sep 12 '18 08:09 johncpang

Thanks for this research! Unfortunately, I've never used the AutoOneToOne model myself, so you know more than me at this point. Would you like to submit a PR with the fix?

skorokithakis avatar Sep 15 '18 23:09 skorokithakis