30

I need to set up one-to-one relation which must also be generic. May be you can advice me a better design. So far I came up to the following models

class Event(models.Model):
    # skip event related fields...
    content_type      = models.ForeignKey(ContentType)
    object_id         = models.PositiveIntegerField()
    content_object    = generic.GenericForeignKey('content_type', 'object_id')

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

class Action1(models.Model):
    # skip action1 related fields...
    events = generic.GenericRelation(Event, content_type_field='content_type', object_id_field='object_id')

    @property
    def event(self):
        return self.events.get() # <<<<<< Is this reasonable?

class Action2(models.Model):...

In Django Admin in event list I want to collect all actions, and from there I want go to admin pages for actions. Is it possible to avoid creating event property in the action models? Is there a better solution? It would be nice to combine the field events and the property event in a single definition. The project I am working with uses Django 1.1

Andrei
  • 10,918
  • 12
  • 76
  • 110
  • If you really want to avoid `events`, you'll need to implement the query to the `Event` table manually, as suggested below. I'd still prefer having `events` as a GenericRelation and then use `self.events.first()` in the `event` property. Also, this would more easily allow you to remove the unique constraint, in the future. – alexcasalboni Jul 13 '17 at 15:53

2 Answers2

21

I recently came across this problem. What you have done is fine, but you can generalise it a little bit more by creating a mixin that reverses the relationship transparently:

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

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

class EventMixin(object):
     @property
     def get_event(self):
         ctype = ContentType.objects.get_for_model(self.__class__)
         try:
             event = Event.objects.get(content_type__pk = ctype.id, object_id=self.id)
         except:
            return None 
         return event

class Action1(EventMixin, models.Model):
    # Don't need to mess up the models fields (make sure the mixing it placed before models.Model)
    ...

and

action = Action1.object.get(id=1)
event = action.get_event

You might want to add caching to the reverse relationship too

Community
  • 1
  • 1
Timmy O'Mahony
  • 53,000
  • 18
  • 155
  • 177
  • Its works well, but it's not give the ability to do: `Action.objects.select_related('event')` – ramusus May 24 '13 at 12:58
  • 5
    I don't like this solution -> a `GenericRelation` looks much better, even if you need to use `self.events.get()` or `self.events.first()` – alexcasalboni Jul 13 '17 at 15:50
  • Same as the comments above. What you had originally with `GenericRelation` is much better. – s-block Feb 27 '18 at 10:13
11

GenericRelation is a class for representing a Generic Many to One and adding a first_event property allow to represent a Generic One to One.

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

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

class Action1(models.Model):
    events = generic.GenericRelation(Event)

    @property
    def first_event(self):
        return self.events.first()

In addition, I recommend using .first() inside Action1.first_event instead of .get() due to, it returning None if the event doesn't exist. .get() raises an exception if the event doesn't exist and this behavior can be unwanted.