31
  • I have a Client and Groupe Model.
  • A Client can be part of multiple groups.
  • Clients that are part of a group can use its group's free rental rate at anytime but only once. That is where the intermediary model (ClientGroupe) comes in with that extra data.

For now, when I try to save the m2m data, it just dies and says I should use the ClientGroupe Manager...so what's missing?

Here are my models:

class Groupe(models.Model):
    nom = models.CharField(max_length=1500, blank=True)

class Client(models.Model):
    nom = models.CharField(max_length=450, blank=True)
    prenom = models.CharField(max_length=450, blank=True)
    groupes = models.ManyToManyField(Groupe, null = True, blank = True, through='ClientGroupe')

class ClientGroupe(models.Model):
    client = models.ForeignKey(Client)
    groupe = models.ForeignKey(Groupe)
    dt = models.DateField(null=True, blank=True) # the date the client is using its group's free rental rate    

    class Meta:
        db_table = u'clients_groupes'

and here's my view:

def modifier(request, id):
    client = Client.objects.get(id=id)    
    form = ClientForm(instance = client)

    dict = {
        "form": form
        , "instance" : client
    }

    if request.method == "POST":
        form = ClientForm(request.POST, instance = client)

        if form.is_valid():
            client_mod = form.save()

            id = client_mod.id
            return HttpResponseRedirect(
                "/client/%(id)s/?err=success" % {"id" : id}
            )
        else:
            return HttpResponseRedirect(
                "/client/%(id)s/?err=warning" % {"id" : id}
            )

    return render_to_response(
        "client/modifier.html"
        , dict
        , context_instance=RequestContext(request)
    )

EDIT:

and here's the ClientForm code:

class ClientForm(ModelForm):
    class Meta:
        model = Client

EDIT #2: here's the error message:

AttributeError at /client/445/

Cannot set values on a ManyToManyField which specifies an intermediary model. Use ClientGroupe's Manager instead.

Request Method:     POST
Request URL:    http://localhost/client/445/
Exception Type:     AttributeError
Exception Value:    Cannot set values on a ManyToManyField which specifies an intermediary model.  Use ClientGroupe's Manager instead.

Exception Location:     C:\Python25\lib\site-packages\django\db\models\fields\related.py  in __set__, line 574
Python Executable:  C:\xampp\apache\bin\apache.exe
Python Version:     2.5.2
Fabian Steeg
  • 44,988
  • 7
  • 85
  • 112
bchhun
  • 18,116
  • 8
  • 28
  • 31
  • Can you tell us the exact error message? Thanks – Ben Jan 06 '09 at 17:58
  • you should pepper you view with print statements a tell us exactly where this ir propagating from, ie before during or after form.is_valid() – Skylar Saveland Sep 13 '09 at 17:35
  • 1
    Related question has a solution at ORM level: http://stackoverflow.com/questions/22964448/add-for-manytomanyfield-which-specifies-an-intermediary-model – guettli Apr 09 '14 at 13:50
  • Can you show your template? Obviously it is not just "{{ form.as_p }}" and "submit" because you want to show the dt field which does not get rendered with the form when using the form variable "form". – Timo Sep 28 '14 at 10:15
  • @Timo this issue has been resolved :) – bchhun Sep 28 '14 at 23:07

5 Answers5

18

If you use the save method right now, Django will try to save using the manager (which Django doesn't allow). Unfortunately, the behavior you want is a little bit trickier than what ModelForm does by default. What you need to do is create a formset.

First of all, you will need to change the options of your ClientForm so that it doesn't display the groupes attribute.

class ClientForm(ModelForm):
    class Meta:
        model = Client
        exclude = ('groupes',)

Next, you must change the view to display the formset:

from django.forms.models import inlineformset_factory

def modifier(request, id):
    client = Client.objects.get(id=id)    
    form = ClientForm(instance = client)
    # Create the formset class
    GroupeFormset = inlineformset_factory(Client, Groupe)
    # Create the formset
    formset = GroupeFormset(instance = client)

    dict = {
        "form": form
        , "formset" : formset
        , "instance" : client
    }

    if request.method == "POST":
        form = ClientForm(request.POST, instance = client)
        formset = GroupeFormset(request.POST, instance = client)

        if form.is_valid() and formset.is_valid():
            client_mod = form.save()
            formset.save()

            id = client_mod.id
            return HttpResponseRedirect(
                "/client/%(id)s/?err=success" % {"id" : id}
            )
        else:
            return HttpResponseRedirect(
                "/client/%(id)s/?err=warning" % {"id" : id}
            )

    return render_to_response(
        "client/modifier.html"
        , dict
        , context_instance=RequestContext(request)
    )

And obviously, you must also tweak your template to render the formset.

If you need any other advice on formsets, see these articles:

Model formsets
Formsets

Ross Light
  • 4,769
  • 1
  • 26
  • 37
  • I have an identical model and the call to inlineformset_factory fails for me. the error is that there is no foreign key between "Client" and "Groupe" – alampada Jun 07 '11 at 22:12
  • 4
    yeah, that doesn't actually work. inlineformset_factory needs a ForeignKey – Cole Maclean Feb 16 '12 at 14:41
  • 1
    GroupeFormset = inlineformset_factory(Client, Groupe) should be GroupeFormset = inlineformset_factory(Client, ClientGroupe). Otherwise, error that antonis_wrx encountered will happen. – Hui Zheng Sep 09 '12 at 13:47
11
…
if form.is_valid():
    client_mod = form.save(commit=False)
    client_mod.save()
    for groupe in form.cleaned_data.get('groupes'):
        clientgroupe = ClientGroupe(client=client_mod, groupe=groupe)
        clientgroupe.save()
    …
Anber
  • 319
  • 2
  • 6
  • 5
    Don't forget to delete the ClientGroupe instances that exist in the db, but are not in cleaned_data, and don't forget to save other m2m-fields in this form too. Moreover, I would prefer putting this in the ClientForm.save() method. – Webthusiast Feb 07 '12 at 13:52
  • This doesn't solve the main issue, the extra field in the through model 'dt' – Mojimi Mar 24 '17 at 18:24
4

You probably need to remove the ManyToMany field from your Client model, or else carefully exclude it from your form. Unfortunately, the default widget for the ManyToMany field cannot populate the ClientGroupe Model properly (even if the missing field, dt, had been set to autonow=True). This is something you'll either need to break out into another form, or handle in your view.

SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
0

When you save your form, you save Client object. Now if you want to assign client to the group you should do this:

clientgroupe = ClientGroupe.objects.create(client=client_instance, groupe=groupe_instance, dt=datetime.datetime.now())

where client_instance and groupe_instance your client and groupe objets.

Vladimir Prudnikov
  • 6,974
  • 4
  • 48
  • 57
-1

I'm providing an alternative solution due to issues I encountered with forms_valid not being called:

class SplingCreate(forms.ModelForm):
class Meta:
    model = SplingModel
    fields = ('Link', 'Genres', 'Image', 'ImageURL',)

def save(self, commit=True):
    from django.forms.models import save_instance

    if self.instance.pk is None:
        fail_message = 'created'
    else:
        fail_message = 'changed'
    fields = set(self._meta.fields) - set(('Genres',))
    instance = save_instance(self, self.instance, fields,
                             fail_message, commit, construct=False)

    genres = self.cleaned_data.get('Genres')
    for genre in genres:
        SplingGenreModel.objects.get_or_create(spling=instance, genre=genre)

    return instance

I've copied the logic from djangos forms/models.py, my field Genres is a manytomany with an intermediary table - I exclude it from the save_instance and then save it separately.

hcliff
  • 433
  • 5
  • 16