I just dealt with a similar problem that included an intermediate model with two foreign keys for the same purpose. This is what my system looks like:
class Node(models.Model): receivers = models.ManyToManyField('self', through='Connection', related_name='senders', symmetrical=False) class Connection(models.Model): sender = models.ForeignKey(Node, related_name='outgoing') receiver = models.ForeignKey(Node, related_name='incoming')
I think this illustrates the basic requirements for using two foreign keys for the same purpose in an intermediate model. That is, the model should have ManyToManyField with the target 'self' (recursive ManyToMany) and the through attribute pointing to the intermediate model. I think it is also necessary that each foreign key be assigned a unique related_name . The argument symmetrical=False applies to recursive relationships if you want them to be one-way, for example. Node1 sends signals to Node2, but Node2 does not necessarily send signals to Node1. It is necessary that the relationship be determined using symmetrical=False , so that the recursive ManyToMany uses a custom “pass-through” model. If you want to create a symmetric recursive ManyToMany with a custom “pass-through” model, advice can be found here .
I found all of these relationships quite confusing, so it took me a while to pick out the reasonable attributes of the model and the names associated with them that actually capture what the code does. To find out how this works, if I have a node N object that calls N.receivers.all() or N.senders.all() returns sets of other Nodes that receive data from N or send data to N, respectively. Call N.outgoing.all() or N.incoming.all() access Connection objects directly through the associated names. Note that there is still some ambiguity that senders and receivers can be replaced with ManyToManyField, and the code will work equally well, but the direction is reversed. I came to the above by checking a test case whether senders really send to “receivers” or vice versa.
In your case, targeting both foreign keys to the user adds to the complexity, as it is unclear how to directly add the recursive ManyToManyField to the user. I think the preferred way to configure the User model is to extend it through a proxy server connected to User through OneToOneField. Perhaps this is unsatisfactory, as expanding membership in MemberhipInfo is unsatisfactory, but at least allows you to easily add additional customization to the user model.
So, for your system, I would try something like this (untested):
class Member(models.Model): user = models.OneToOneField(User, related_name='member') recruiters = models.ManyToManyField('self', through = 'Membership', related_name = 'recruits', symmetrical=False) other_custom_info = ... class UserManagedGroup(Group): leader = models.ForeignKey(Member, related_name='leaded_groups') members = models.ManyToManyField(Member, through='Membership', related_name='managed_groups') class Membership(models.Model): member = models.ForeignKey(Member, related_name='memberships') made_member_by = models.ForeignKey(Member, related_name='recruitments') group = models.ForeignKey(UserManagedGroup, related_name='memberships') date_added = ... membership_justification = ...
The recursive field must be asymmetric, as Member1 recruiting Member2 should not also mean that Member2 has recruited Member1. I changed a few attributes to convey relationships more clearly. You can use a member of the proxy server wherever you use the user, since you can always access Member.user if you need to get to the user object. If this works as intended, you should be able to do the following with this member M:
M.recruiters.all() -> set of other members that have recruited M to groups M.recruits.all() -> set of other members that M has recruited to groups M.leaded_groups.all() -> set of groups M leads M.managed_groups.all() -> set of groups of which M is a member M.memberships.all() -> set of Membership objects in which M has been recruited M.recruitments.all() -> set of Membership objects in which M has recruited someone
And for group G,
G.memberships.all() -> set of Memberships associated with the group
I think this should work and provide a “cleaner” solution than a separate MembershipInfo model, but this may require some tuning, for example, checking the direction of the recursive field to make sure that recruiters recruit recruits, and not vice versa,
Edit: I forgot to associate the member model with the user model. This will be done as follows:
def create_member(member, instance, created, **kwargs): if created: member, created = Member.objects.get_or_create(user=instance) post_save.connect(create_member, member=User)
Note that create_member is not a Member method, but is called after a member is defined. In doing so, a member object must be automatically created whenever a User is created (you may need to set the member fields to null = True and / or blank = True if you want to add users without initializing Member fields).