I'm trying to create a messaging system where a message's sender and recipients can be generic entities. This seems fine for the sender, where there is only object to reference (GenericForeignKey) but I can't figure out how to go about this for the recipients (GenericManyToManyKey ??)
Below is a simplified example. PersonClient and CompanyClient inherit attributes from Client but have their own specific details. The last line is the sticking point. How do you allow message recipients to be a set of CompanyClients and PersonClients
class Client(models.Model):
city = models.CharField(max_length=16)
class Meta:
abstract = True
class PersonClient(Client):
first_name = models.CharField(max_length=16)
last_name = models.CharField(max_length=16)
gender = models.CharField(max_length=1)
class CompanyClient(Client):
name = models.CharField(max_length=32)
tax_no = PositiveIntegerField()
class Message(models.Model):
msg_body = models.CharField(max_length=1024)
sender = models.ForeignKey(ContentType)
recipients = models.ManyToManyField(ContentType)
You can implement this using generic relationships by manually creating the junction table between message and recipient:
from django.db import models
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
class 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 = True
class 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.name
class 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 models
from 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With