Generic many-to-many relationships Generic many-to-many relationships python python

Generic many-to-many relationships


You can implement this using generic relationships by manually creating the junction table between message and recipient:

from django.db import modelsfrom django.contrib.contenttypes import genericfrom django.contrib.contenttypes.models import ContentTypeclass Client(models.Model):    city = models.CharField(max_length=16)    # These aren't required, but they'll allow you do cool stuff    # like "person.sent_messages.all()" to get all messages sent    # by that person, and "person.received_messages.all()" to    # get all messages sent to that person.    # Well...sort of, since "received_messages.all()" will return    # a queryset of "MessageRecipient" instances.    sent_messages = generic.GenericRelation('Message',        content_type_field='sender_content_type',        object_id_field='sender_id'    )    received_messages = generic.GenericRelation('MessageRecipient',        content_type_field='recipient_content_type',        object_id_field='recipient_id'    )    class Meta:        abstract = Trueclass PersonClient(Client):    first_name = models.CharField(max_length=16)    last_name = models.CharField(max_length=16)    gender = models.CharField(max_length=1)    def __unicode__(self):        return u'%s %s' % (self.last_name, self.first_name)class CompanyClient(Client):    name = models.CharField(max_length=32)    tax_no = models.PositiveIntegerField()    def __unicode__(self):        return self.nameclass Message(models.Model):    sender_content_type = models.ForeignKey(ContentType)    sender_id = models.PositiveIntegerField()    sender = generic.GenericForeignKey('sender_content_type', 'sender_id')    msg_body = models.CharField(max_length=1024)    def __unicode__(self):        return u'%s...' % self.msg_body[:25]class MessageRecipient(models.Model):    message = models.ForeignKey(Message)    recipient_content_type = models.ForeignKey(ContentType)    recipient_id = models.PositiveIntegerField()    recipient = generic.GenericForeignKey('recipient_content_type', 'recipient_id')    def __unicode__(self):        return u'%s sent to %s' % (self.message, self.recipient)

You'd use the above models like so:

>>> person1 = PersonClient.objects.create(first_name='Person', last_name='One', gender='M')>>> person2 = PersonClient.objects.create(first_name='Person', last_name='Two', gender='F')>>> company = CompanyClient.objects.create(name='FastCompany', tax_no='4220')>>> company_ct = ContentType.objects.get_for_model(CompanyClient)>>> person_ct = ContentType.objects.get_for_model(person1) # works for instances too.# now we create a message:>>> msg = Message.objects.create(sender_content_type=person_ct, sender_id=person1.pk, msg_body='Hey, did any of you move my cheese?')# and send it to a coupla recipients:>>> MessageRecipient.objects.create(message=msg, recipient_content_type=person_ct, recipient_id=person2.pk)>>> MessageRecipient.objects.create(message=msg, recipient_content_type=company_ct, recipient_id=company.pk)>>> MessageRecipient.objects.count()2

As you can see, this is a far more verbose (complicated?) solution. I'd probably keep it simple and go with Prariedogg's solution below.


The absolute best way to go about this is to use a library called django-gm2m

pip install django-gm2m

Then if we have our models

>>> from django.db import models>>>>>> class Video(models.Model):>>>       class Meta:>>>           abstract = True>>>>>> class Movie(Video):>>>     pass>>>>>> class Documentary(Video):>>>     pass

And a user

>>> from gm2m import GM2MField>>>>>> class User(models.Model):>>>     preferred_videos = GM2MField()

We can do

>>> user = User.objects.create()>>> movie = Movie.objects.create()>>> documentary = Documentary.objects.create()>>>>>> user.preferred_videos.add(movie)>>> user.preferred_videos.add(documentary)

Sweet right?

For more info go here:

http://django-gm2m.readthedocs.org/en/stable/quick_start.html


You might get around this problem by simplifying your schema to include a single Client table with a flag to indicate what type of client it was, instead of having two separate models.

from django.db import modelsfrom django.utils.translation import ugettext_lazy as _class Client(models.Model):    PERSON, CORPORATION = range(2)    CLIENT_TYPES = (                    (PERSON, _('Person')),                    (CORPORATION, _('Corporation')),                   )    type = models.PositiveIntegerField(choices=CLIENT_TYPES, default=PERSON)    city = models.CharField(max_length=16)    first_name = models.CharField(max_length=16, blank=True, null=True)    last_name = models.CharField(max_length=16, blank=True, null=True)    corporate_name = models.CharField(max_length=16, blank=True, null=True)    tax_no = models.PositiveIntegerField(blank=True, null=True)    def save(self, *args, **kwargs):        """        Does some validation ensuring that the person specific fields are        filled in when self.type == self.PERSON, and corporation specific        fields are filled in when self.type == self.CORPORATION ...        """        # conditional save logic goes here        super(Client, self).save(*args, **kwargs)

If you do things this way you might not have to mess around with Generic Foreign Keys at all. As an added convenience you can also write custom managers for the Client model like Client.corporate.all(), Client.person.all(), to return pre-filtered querysets containing only the type of clients that you want.

This also may not be the best way of solving your problem. I'm just throwing it out there as one potential possibility. I don't know if there's conventional wisdom about smashing together two similar models and using a save override to ensure data integrity. It seems like it could be potentially problematic ... I'll let the community learn me on this one.