Creating recursive, symmetrical many-to-many relationships in Django
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!