Django models: how to overcome the "through" limitation of ManyToMany

I am working on an application that allows users to create and manage user groups themselves.

The problem is that I want to keep which user added the new member to any group.

These are my models at the moment:

class UserManagedGroup(Group): leader = models.ForeignKey(User, verbose_name=_('group leader'), related_name='leaded_groups') members = models.ManyToManyField(User, verbose_name=_('members'), through='Membership', related_name='managed_groups') class Membership(models.Model): user = models.ForeignKey(User, related_name='memberships') group = models.ForeignKey(UserManagedGroup, related_name='memberships') info = models.OneToOneField('MembershipInfo', verbose_name=_('membership information')) class Meta: unique_together = ['user', 'group'] class MembershipInfo(models.Model): date_added = models.DateField(_('date added'), auto_now_add=True) made_member_by = models.ForeignKey(User, verbose_name=_('user who made him a member')) membership_justification = models.TextField(_('membership justification'), blank=True, default='') @receiver(signals.post_delete, sender=Membership) def delete_membership_info(sender, instance, *args, **kwargs): if instance.info.pk: instance.info.delete() 

As you can see, I have a silly MembershipInfo model that will be much better merged with Membership due to the nature of its fields. In addition, MembershipInfo life is tied to its Membership (which is why I had to create this post_delete connection).

I cannot combine them because of this:

Your intermediate model should contain one - and only one - foreign key for the target model (in this example it will be Person). If you have more than one foreign key, a verification error will be confirmed.

(In my case, I cannot use 2 foreign keys for the user)

Now it works, but I don't like it. This makes creating an instance of Membership tedious, since I always have to create an instance of MembershipInfo . In addition, 2 queries instead of 1.

QUESTION The best way to store 2 foreign keys for the same model ( User ), tied to my member relationships.

+4
source share
2 answers

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).

+5
source

The easiest way I see is to remove the ManyToMany field from your UserManagedGroup and merge the membership and the Info membership.

You can also access your members and entry_set fields.

+2
source

All Articles