Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reversing a unique generic foreign key (and returning an object as opposed to a related manager)

Tags:

django

I have a model that has a unique generic foreign key relationship:

class Contact(models.Model):
    ...
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey()

    class Meta:
         unique_together = ('content_type', 'object_id',)

meaning that a Contact can only ever belong to one object. Usually, when I want to reverse the relationship I can do

class Person(models.Model):
    ...
    contacts = generic.GenericRelation(Contact)

and calling person.contacts.all() will give me all the objects. Because only one Contact will ever be returned in my situation, is there a better way of accessing this object in reverse?

p.s. I could write person.contact.all()[0] but there must be a cleaner approach

like image 301
Timmy O'Mahony Avatar asked Feb 08 '12 18:02

Timmy O'Mahony


3 Answers

Well, looking at the question in Chris's comment helped, and I wrote a mixin to return the object using a quick lookup (which should be cached):

class ContactMixin(object):
    @property
    def contactcard(self):
        ctype = ContentType.objects.get_for_model(self.__class__)
        try:
            contact = Contact.objects.get(content_type__pk = ctype.id, object_id=self.id)
        except Contact.DoesNotExist:
            return None 
        return contact

and the Person:

 class Person(ContactMixin, models.Model):
     ...

now I can just call

myperson.contactcard.phone_number 

I won't accept just yet, as there might be other suggestions

like image 134
Timmy O'Mahony Avatar answered Nov 18 '22 10:11

Timmy O'Mahony


if a queryset contains exactly one element you can do qs.get(), or in your case

person.contact.get()

(you may still need to catch the DoesNotExists exception)

like image 5
second Avatar answered Nov 18 '22 12:11

second


The accepted answer above implicitly assumes that the contact field is nullable whereas in the original post this is not the case. Assuming that you do want the key to be nullable I would do this:

class Contact(models.Model):
    ...
    content_type = models.ForeignKey(
        ContentType,
        blank=True,
        null=True,
        on_delete=models.SET_NULL
    )
    object_id = models.PositiveIntegerField(blank=True, null=True)
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        unique_together = ('content_type', 'object_id',)


class Person(models.Model):
    ...
    contacts = generic.GenericRelation(Contact)

    @property
    def contact(self):
       return self.contacts.first()


# Or if you want to generalise the same thing to a mixin.
class ContactableModel(models.Model):
    contacts = generic.GenericRelation(Contact)

    class Meta:
        abstract = True

    @property
    def contact(self):
       return self.contacts.first()

If there is no contact then .first() will return None. Simple and pythonic. There's no need for all the generic introspection mechanics of the accepted answer, the references are already right at our fingertips.

like image 4
jhrr Avatar answered Nov 18 '22 12:11

jhrr