14

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

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
Timmy O'Mahony
  • 53,000
  • 18
  • 155
  • 177
  • Possible duplicate of http://stackoverflow.com/questions/4893823/how-can-i-make-and-enforce-a-generic-onetoone-relation-in-django – Chris Pratt Feb 08 '12 at 18:25
  • I know the answer to my question is (or seems to be) in the comments, but the question itself is different (thanks for the link - I'll have a look now) – Timmy O'Mahony Feb 08 '12 at 18:29

3 Answers3

13

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

Timmy O'Mahony
  • 53,000
  • 18
  • 155
  • 177
  • 2
    it's generally good practice to specify the exceptions your are expecting (in this case doesnotexists and multipleobjectsreturned). That way you don't accidentally end up hiding other exceptions which you probably want bubbling up – second Dec 09 '12 at 14:45
  • I have the same problem and looking at this solution, I don't think `select_related` nor `prefetch_related` works on this :( It would get expensive real fast to get the `contactcard` property for a large queryset. – user193130 Dec 11 '14 at 03:49
5

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)

second
  • 28,029
  • 7
  • 75
  • 76
  • 1
    Interesting, didn't know that. Unfortunatley `person.contact` is returning a Manager/GenericRelation object so I would still need to use `person.contacts.all.get` which is equally cumbersome – Timmy O'Mahony Feb 08 '12 at 18:27
4

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.

jhrr
  • 1,624
  • 14
  • 22