17

I'm having trouble in saving a m2m data, containing a 'through' class table. I want to save all selected members (selected in the form) in the through table. But i don't know how to initialise the 'through' table in the view.

my code:

class Classroom(models.Model):
     user = models.ForeignKey(User, related_name = 'classroom_creator')
     classname = models.CharField(max_length=140, unique = True)
     date = models.DateTimeField(auto_now=True)
     open_class = models.BooleanField(default=True)
     members = models.ManyToManyField(User,related_name="list of invited members", through = 'Membership')

class Membership(models.Model): 
      accept = models.BooleanField(User)
      date = models.DateTimeField(auto_now = True) 
      classroom = models.ForeignKey(Classroom, related_name = 'classroom_membership')
      member = models.ForeignKey(User, related_name = 'user_membership')

and in the view:

def save_classroom(request):
   classroom_instance = Classroom()
   if request.method == 'POST':
        form = ClassroomForm(request.POST, request.FILES, user = request.user) 
        if form.is_valid():
           new_obj = form.save(commit=False)
           new_obj.user = request.user 
           new_obj.save()
           membership = Membership(member = HERE SELECTED ITEMS FROM FORM,classroom=new_obj)

           membership.save() 

How should I initialise the membership for the Membership table to be populated right?

tshepang
  • 12,111
  • 21
  • 91
  • 136
dana
  • 5,168
  • 20
  • 75
  • 116

4 Answers4

23

In case of using normal m2m relation (not through intermediary table) you could replace:

membership = Membership(member = HERE SELECTED ITEMS FROM FORM,classroom=new_obj)
membership.save()

with

form.save_m2m()

But in case of using intermediary tables you need to manually handle POST data and create Membership objects with all required fields (similar problem). The most basic solution is to change your view to something like:

def save_classroom(request):
    if request.method == 'POST':
        form = ClassroomForm(request.POST, request.FILES)

        if form.is_valid():
           new_obj = form.save(commit=False)
           new_obj.user = request.user 
           new_obj.save()

           for member_id in request.POST.getlist('members'):
                membership = Membership.objects.create(member_id = int(member_id), classroom = new_obj)
           return HttpResponseRedirect('/')
    else:
        form = ClassroomForm()
    return render_to_response('save_classroom.html', locals())

Note how request.POST is manipulated (.getlist). This is because post and get are QueryDict objects which has some implications (request.POST['members'] will return always one object!).

You can modify this code to get it more reliable (error handling etc.), and more verbose, eg:

member = get_object_or_404(User, pk = member_id)
membership = Membership.objects.create(member = member , classroom = new_obj)

But note that you are performing some db queries in a loop which is not a good idea in general (in terms of performance).

dzida
  • 8,854
  • 2
  • 36
  • 57
  • that was it! It really created me some headaches, as i'm quite new to python. Thnx a lot! – dana Jun 26 '10 at 21:36
  • Question: If i want to add the creator of the classroom as a member (the required.user), and to that list, how can i do it? Thanks! – dana Jun 28 '10 at 21:52
  • 1
    Not sure if I got your point. Either you can create new membership object (outside the loop) to create new connection between user and classroom (membership = Membership.objects.create(member = request.user, classroom = new_obj) ) or you can pass extra member_id(which is id of request.user) in POST and relation user-classroom will be created with use of existing code (for loop). For the second option you need to store request.user.id in some hidden field in your form. – dzida Jun 28 '10 at 22:12
  • yes! that was it! i was hesitating in double declarating the 'membership', but it is the right solution! thanks again! – dana Jun 29 '10 at 08:50
  • Sorry to re-raise a question from years ago but what if you want to update rather than create a record? If a record already exist, doesn't objects.create add a new record rather than update? – nlr25 Aug 27 '13 at 20:51
5

Like what dzida did, but use form.cleaned_data instead of request.post:

def save_classroom(request):
    if request.method == 'POST':
        form = ClassroomForm(request.POST, request.FILES)

        if form.is_valid():
           new_obj = form.save(commit=False)
           new_obj.user = request.user
           new_obj.save()

           for member in form.cleaned_data['members'].all():
                Membership.objects.create(member = member,  classroom = new_obj)

           return HttpResponseRedirect('/')
    else:
        form = ClassroomForm()
    return render_to_response('save_classroom.html', locals())

You also need to consider some memberships might be deleted, so:

def save_classroom(request):
    if request.method == 'POST':
        form = ClassroomForm(request.POST, request.FILES)

        if form.is_valid():
           new_obj = form.save(commit=False)
           new_obj.user = request.user
           new_obj.save()

           final_members = form.cleaned_data['members'].all()
           initial_members = form.initial['members'].all()

           # create and save new members
           for member in final_members:
                if member not in initial_members:
                    Membership.objects.create(member = member,  classroom = new_obj)

           # delete old members that were removed from the form
           for member in initial_members:
               if member not in final_members:
                   Membership.objects.filter(member = member, classroom = new_obj).delete()

           return HttpResponseRedirect('/')
    else:
        form = ClassroomForm()
    return render_to_response('save_classroom.html', locals())

If you use model forms (like in a generic CBV: form_class=ClassroomForm), override and put the saving logic above in the save method, something like:

ClassroomForm(forms.ModelForm):
    members = ModelMultipleChoiceField(
        queryset=Classroom.objects.all(),
        widget=SelectMultiple
    )

    def save(self, commit=True):
        classroom = super().save(commit=False)
            if commit:
                classroom.save()
                if 'members' in self.changed_data:
                   final_members = self.cleaned_data['members'].all()
                   initial_members = self.initial['members']

                   # create and save new members
                   for member in final_members:
                        if member not in initial_members:
                            Membership.objects.create(member = member,  classroom = new_obj)

                   # delete old members that were removed from the form
                   for member in initial_members:
                       if member not in final_members:
                           Membership.objects.filter(member = member, classroom = new_obj).delete()

         return classroom
ElectRocnic
  • 1,275
  • 1
  • 14
  • 25
mehmet
  • 7,720
  • 5
  • 42
  • 48
1

You also need to specify the classroom for the membership:

membership = Membership(member = request.user,
                        classroom=new_obj) #if new_obj if your classroom
membership.save()

I guess you should also remove User in accept = models.BooleanField(User). It shouldn't be necessary to set the date upon saving if you are using auto_now! But maybe `auto_now_add is more likely what you need (http://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.DateField)

Bernhard Vallant
  • 49,468
  • 20
  • 120
  • 148
  • thanks it seems the natural solution, the problem is that new_obj - my classroom is not created yet, i guess. now i've added the above declaration after new_obj.save(), and the new error is: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. :) – dana Jun 19 '10 at 13:47
  • Try with Membership.objects.create() then, but do not forget to specify the classromm object! – Bernhard Vallant Jun 19 '10 at 18:12
  • Sorry to re-raise a question from years ago but what if you want to update rather than create a record? If a record already exist, doesn't objects.create add a new record rather than update? – nlr25 Aug 27 '13 at 20:54
0

This is how I did it in a generic UpdateForm class-based view (django 1.8) for a similar yet different application using the form_valid method.

def form_valid(self, form):
    """
    If the form is valid, save the associated model.
    """
    self.object.members.clear()
    self.object = form.save(commit=False)
    self.object.user = self.request.user 
    self.object.save()

    list_of_members = form.cleaned_data['members']

    ClassRoom.objects.bulk_create([
            Membership(
                Course=self.object,
                member=member_person,
                order=num)
            for num, member_person in enumerate(list_of_members)
        ])
    return super(ModelFormMixin, self).form_valid(form)
Nate
  • 51
  • 5