Django enforcing unique constraint on set of members in ManyToManyField

Viewed 540

I am working in Django and I'm having some trouble enforcing a particular constraint on my data models. I have a model that consists of a group of members.

class Member:
    name = models.CharField(max_length=100)

class Team:
    members = models.ManyToManyField(Member)

I want to enforce a constraint such that, for any given team, the set of members is unique. That is, the following teams are allowed:

team_a: {A, B, C}
team_b: {A, B, D}

But a team_c: {C, B, A} is not allowed because the set of members is the same as team_a.

How can I enforce this constraint such that an error is thrown if I try to add team_c (or any other team with the same set of members)?

I have considered having a foreign key for each member and then using unique_together, but this doesn't work because I'd like the model to be unaware of ordering within the members (as there is no ordering inherent in the data). I think this might be a job for the m2m_changed signal, but I haven't been able to get it to work properly.

1 Answers

Hmm there is no direct way, but on top of my head this might work.

The only possibility of a set of members clash with another team members is only when the member which you are about to add exists in other teams, so first we need to fetch those teams in which user is already added:

# fetch teams where user is already added
teams = Team.objects.filter(members__id=user.id)

will_clash = False
if teams:
    # prepare list of tuples of sorted members id for each team
    members_set = [tuple(team.members.values_list('id', flat=True).order_by('id')) for team in teams]

    # we know already in which team we want to add lets say it is `team_c`
    team_c_members_set = tuple(sorted(list(team_c.members.values_list('id', flat=True)) + [user.id]))

    if team_c_members_set in members_set:
        # adding current `user` is going to create a clash
        will_clash = True

if not will_clash:
    # all good lets add the user in team
    team_c.members.add(user)

Dry Run:

Lets say you have following three teams:

    Team A:
        1: member
        2: member
        3: member

    Team B:
        1: member
        2: member
        4: member

    Team C:
        3: member
        2: member

Now we want to add member id: 1 into team C. Which should be avoided as that will create same set of users as in team A, lets run the above code to see if that is True:

# fetch teams where user is already added
teams = [A, B]  # user already there

# prepare list of tuples of sorted members id for each team
members_set = [(1, 2, 3), (1, 2, 4)]

# we know already in which team we want to add lets say it is `team_c` so it will become
team_c_members_set = (1, 2, 3)  # tuple(sorted([3, 2] + [1]))

team_c_members_set in members_set  # <-- True

Scenario adding full team at once

Lets say you are adding full team C at once, so you already know which members you will have in that team, in that case process is almost similar but team_c_members_set is created first to be used in filtering teams:

users_to_add = [user_a, user_b, user_c]
team_c_members_set = tuple(sorted([u.id for u in users_to_add])
teams = Team.objects.filter(members__id__in=team_c_members_set).distinct()
will_clash = False
if teams:
    # prepare list of tuples of sorted members id for each team
    members_set = [tuple(team.members.values_list('id', flat=True).order_by('id')) for team in teams]
    if team_c_members_set in members_set:
        will_clash = True

if not will_clash:
    # all good lets add the users in team
    team_c.members.add(*users_to_add)
Related