Django - Add entry to ManyToMany field through class based view

539 Views Asked by At

I want to add / remove members from a Team model. Members are specified as a ManyToManyField. I use django-rules to specify permissions, so team owners should be able to add/remove members.

# models.py

from django.db import models
from rules.contrib.models import RulesModel
from django.conf import settings

class Team(RulesModel):
    name = models.CharField(max_length=80)
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        help_text="Owner can view, change or delete this team.",
        related_name="team_owner",
    )
    members = models.ManyToManyField(
        settings.AUTH_USER_MODEL, blank=True, related_name="team_members"
    )

The permissions are specified as following:

import rules

@rules.predicate
def is_team_owner(user, obj):
    return obj.owner == user

rules.add_perm("teamapp.change_team", is_team_owner)

I've specified some generic views (CreateView, DetailView, UpdateView and DeleteView) to manage the Team. Now I want two separate views to add and remove members on the same.

# views.py

from django.views.generic import (
    CreateView,
    DetailView,
    UpdateView,
    ListView,
    DeleteView,
)
from rules.contrib.views import PermissionRequiredMixin
from django.contrib.auth import get_user_model
from .models import Team

class TeamMemberAddView(PermissionRequiredMixin, UpdateView):
    model = Team
    permission_required = "teamapp.change_team"
    raise_exception = True
    fields = ["members"]

    def form_valid(self, form):
        user = get_user_model()
        new_member = user.objects.get(pk=1)
        self.object.members.add(new_member)
        return super(TeamMemberAddView, self).form_valid(form)

Which generic view can I use to add / remove members? Which approach is recommended here? I wanted 1 dedicated view to select an existing User to be added, and some links on the list view to delete members. My approach fails, because it does not add members, it only updates to the last User selected. So the ManyToMany table only contains one record.

1

There are 1 best solutions below

0
Aurelien On

TL;DR: replace the last line of form_valid by return HttpResponseRedirect(self.get_success_url())

It's important to understand how form_valid of UpdateView works. I recommend to visualize the methods on ccbv.co.uk.

From ModelFormMixin:

If the form is valid, save the associated model.

def form_valid(self, form):
    """If the form is valid, save the associated model."""
    self.object = form.save()
    return super().form_valid(form)

It means that the object will be saved with the data submitted by the form. UpdateView will restrict the changes to the fields variable:

fields = ["members"]

From FormMixin:

If the form is valid, redirect to the supplied URL.

def form_valid(self, form):
    """If the form is valid, redirect to the supplied URL."""
    return HttpResponseRedirect(self.get_success_url())

For your concrete case (add a many-to-many relationship), you need to bypass the model saving from ModelFormMixin by simply returning the supplied URL after adding the relationship (last line changed):

def form_valid(self, form):
    user = get_user_model()
    new_member = user.objects.get(pk=1)
    self.object.members.add(new_member)
    return HttpResponseRedirect(self.get_success_url())

Side note: your form seems to provide the member object you want to add, so you could use this instead of including it in the url. Try:

def form_valid(self, form):
    for member in form.cleaned_data['members'].all():
        self.object.members.add(member.id)
    return HttpResponseRedirect(self.get_success_url())