Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django ModelForms: Display ManyToMany field as single-select

In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:

class Bet(models.Model):
    ...
    participants = models.ManyToManyField(User)

User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.

I'm using a ModelForm for my NewBetForm:

class NewBetForm(forms.ModelForm):
    class Meta:
        model = Bet
        widgets = {
            'participants': forms.Select()
        }

    def save(self, user):
        ... # save user as participant

Notice the redefined widget for the participants field which makes sure you can only choose one participant.

However, this gives me a validation error:

Enter a list of values.

I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.

I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.

Can anyone tell me what the problem is exactly and if there is a good way to solve it?

Thanks in advance!

Edit

As suggested in the comments: the (relevant) code of the view.

def new_bet(request):
    if request.method == 'POST':
        form = NewBetForm(request.POST)
        if form.is_valid():
            form.save(request.user)
            ... # success message and redirect
    else:
        form = NewBetForm()
    return render(request, 'bets/new.html', {'form': form})
like image 803
j0ker Avatar asked Feb 20 '23 00:02

j0ker


2 Answers

After digging in the Django code, I can answer my own question.

The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.

To solutions I could think of:

  1. Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
  2. Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.

The seconds solution seems to be less verbose, so that's what I will be going with.

like image 77
j0ker Avatar answered Feb 26 '23 21:02

j0ker


I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.

In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.

forms.py

from django.utils.datastructures import MultiValueDict, MergeDict

class M2MSelect(forms.Select):
    def value_from_datadict(self, data, files, name):
        if isinstance(data, (MultiValueDict, MergeDict)):
            return data.getlist(name)
        return data.get(name, None)    

class WindowsSubnetForm(forms.ModelForm):
    port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
    class Meta:
        model = Subnet
like image 41
Ryan Currah Avatar answered Feb 26 '23 22:02

Ryan Currah