In Django, a recursive many-to-many relationship is a ManyToManyField that points to the same model in which it's defined ('self'). A symmetrical relationship is one in where, when a.contacts = [b], a is in b.contacts.
In changeset 8136, support for through models was added to the Django core. This allows you to create a many-to-many relationship that goes through a model of your choice:
class Contact(models.Model):
contacts = models.ManyToManyField('self', through='ContactRelationship',
symmetrical=False)
class ContactRelationship(models.Model):
types = models.ManyToManyField('RelationshipType', blank=True,
related_name='contact_relationships')
from_contact = models.ForeignKey('Contact', related_name='from_contacts')
to_contact = models.ForeignKey('Contact', related_name='to_contacts')
class Meta:
unique_together = ('from_contact', 'to_contact')
According to the Django Docs, you must set symmetrical=False for recursive many-to-many relationships that use an intermediary model. Sometimes--for a recent case in django-crm, for example--what you really want is a symmetrical, recursive many-to-many relationship.
The trick to getting this working is understanding what symmetrical=True actually does. From what we can tell after a brief look through the Django core, symmetrical=True is simply a utility that (a) creates a second, reverse relationship in the many-to-many table, and (b) hides the field in the related model (in this case the same model) from use by appending a '+' to its name.
Since you normally have to create many-to-many relationships manually when a through model is specified, the solution is simply to leave symmetrical=False (otherwise it'll raise an exception) and create the reverse relationship manually yourself via the through model:
crm.ContactRelationship.objects.create(from_contact=contact_a, to_contact=contact_b)
crm.ContactRelationship.objects.create(from_contact=contact_b, to_contact=contact_a)
Additionally, you'll have to do a little cleanup to make sure both sides of the relationship are removed when one is removed, but otherwise this should achieve the same effect as setting symmetrical=True in other many-to-many relationships.
To hide the other side of the related manager, you can append a '+' to the related_name, like so:
class Contact(models.Model):
contacts = models.ManyToManyField('self', through='ContactRelationship',
symmetrical=False,
related_name='related_contacts+')
Good luck and feel free to comment with any questions!