Django "Enter a list of values" form error when rendering a ManyToManyField as a Textarea

11,993

The probable problem is that the list of values provided in the text area can not be normalized into a list of Models.

See the ModelMultipleChoiceField documentation.

The field is expecting a list of valid IDs, but is probably receiving a list of text values, which django has no way of converting to the actual model instances. The to_python will be failing within the form field, not within the form itself. Therefore, the values never even reach the form.

Is there something wrong with using the built in ModelMultipleChoiceField? It will provide the easiest approach, but will require your users to scan a list of available actors (I'm using the actors field as the example here).

Before I show an example of how I'd attempt to do what you want, I must ask; how do you want to handle actors that have been entered that don't yet exist in your database? You can either create them if they exist, or you can fail. You need to make a decision on this.

# only showing the actor example, you can use something like this for other fields too

class MovieModelForm(forms.ModelForm):
    actors_list = fields.CharField(required=False, widget=forms.Textarea())

    class Meta:
        model = MovieModel
        exclude = ('actors',)

    def clean_actors_list(self):
        data = self.cleaned_data
        actors_list = data.get('actors_list', None)
        if actors_list is not None:
            for actor_name in actors_list.split(','):
                try:
                    actor = Actor.objects.get(actor=actor_name)
                except Actor.DoesNotExist:
                    if FAIL_ON_NOT_EXIST: # decide if you want this behaviour or to create it
                        raise forms.ValidationError('Actor %s does not exist' % actor_name)
                    else: # create it if it doesnt exist
                        Actor(actor=actor_name).save()
        return actors_list

    def save(self, commit=True):
        mminstance = super(MovieModelForm, self).save(commit=commit)
        actors_list = self.cleaned_data.get('actors_list', None)
        if actors_list is not None:
            for actor_name in actors_list.split(","):
                actor = Actor.objects.get(actor=actor_name)
                mminstance.actors.add(actor)

        mminstance.save()
        return mminstance

The above is all untested code, but something approaching this should work if you really want to use a Textarea for a ModelMultipleChoiceField. If you do go down this route, and you discover errors in my code above, please either edit my answer, or provide a comment so I can. Good luck.

Edit:

The other option is to create a field that understands a comma separated list of values, but behaves in a similar way to ModelMultipleChoiceField. Looking at the source code for ModelMultipleChoiceField, it inhertis from ModelChoiceField, which DOES allow you to define which value on the model is used to normalize.

## removed code because it's no longer relevant. See Last Edit ##

Edit:

Wow, I really should have checked the django trac to see if this was already fixed. It is. See the following ticket for information. Essentially, they've done the same thing I have. They've made ModelMutipleChoiceField respect the to_field_name argument. This is only applicable for django 1.3!

The problem is, the regular ModelMultipleChoiceField will see the comma separated string, and fail because it isn't a List or Tuple. So, our job becomes a little more difficult, because we have to change the string to a list or tuple, before the regular clean method can run.

class ModelCommaSeparatedChoiceField(ModelMultipleChoiceField):
    widget = Textarea
    def clean(self, value):
        if value is not None:
            value = [item.strip() for item in value.split(",")] # remove padding
        return super(ModelCommaSeparatedChoiceField, self).clean(value)

So, now your form should look like this:

class MovieModelForm(forms.ModelForm):
    actors = ModelCommaSeparatedChoiceField(
               required=False, 
               queryset=Actor.objects.filter(), 
               to_field_name='actor')
    equipments = ModelCommaSeparatedChoiceField(
               required=False,
               queryset=Equipment.objects.filter(),
               to_field_name='equip')
    lights = ModelCommaSeparatedChoiceField(
               required=False, 
               queryset=Light.objects.filter(),
               to_field_name='light')

    class Meta:
        model = MovieModel
Share:
11,993

Related videos on Youtube

sharkfin
Author by

sharkfin

Updated on May 28, 2022

Comments

  • sharkfin
    sharkfin almost 2 years

    I'm trying to learn Django and I've ran into some confusing points. I'm currently having trouble creating a movie using a form. The idea of the form is to give the user any field he'd like to fill out. Any field that the user fills out will be updated in its respective sql table (empty fields will be ignored). But, the form keeps giving me the error "Enter a list of values" when I submit the form. To address this, I thought stuffing the data from the form into a list and then returning that list would solve this.

    The first idea was to override the clean() in my ModelForm. However, because the form fails the is_valid() check in my views, the cleaned_data variable in clean() doesn't contain anything. Next, I tried to override the to_python(). However, to_python() doesn't seem to be called.

    If I put __metaclass__ = models.SubfieldBase in the respective model, I receive the runtime error

    "TypeError: Error when calling the metaclass bases metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases"

    My approach doesn't seem to work. I'm not sure how to get around the 'Enter a list of values" error! Any advice?

    Here is the relevant code (updated):

    models.py
    
    """ Idea:
    A movie consists of many equipments, actors, and lighting techniques. It also has a rank for the particular movie, as well as a title. 
    
    A Theater consists of many movies.
    
    A nation consists of many theaters. 
    """
    
    from django.db import models
    from django.contrib.auth.models import User
    
    class EquipmentModel(models.Model):
            equip = models.CharField(max_length=20)
    #       user = models.ForeignKey(User)
    
    class ActorModel(models.Model):
            actor = models.CharField(max_length=20)
    #       user = models.ForeignKey(User)
    
    class LightModel(models.Model):
            light = models.CharField(max_length=20)
    #       user = models.ForeignKey(User)
    
    class MovieModel(models.Model):
    #       __metaclass__ = models.SubfieldBase   
            rank = models.DecimalField(max_digits=5000, decimal_places=3)
            title = models.CharField(max_length=20)
    
            equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
            actors = models.ManyToManyField(ActorModel, blank=True, null=True)
            lights = models.ManyToManyField(LightModel, blank=True, null=True)
    
    class TheaterModel(models.Model):
            movies = models.ForeignKey(MovieModel)
    
    class NationModel(models.Model):
            theaters = models.ForeignKey(TheaterModel)
    
    =====================================
    forms.py
    
    """ 
    These Modelforms tie in the models from models.py
    
    Users will be able to write to any of the fields in MovieModel when creating a movie.
    Users may leave any field blank (empty fields should be ignored, ie: no updates to database).
    """
    
    from django import forms
    from models import MovieModel
    from django.forms.widgets import Textarea
    
    class MovieModelForm(forms.ModelForm):
          def __init__(self, *args, **kwargs):
                 super(MovieModelForm, self).__init__(*args, **kwargs)
                 self.fields["actors"].widget = Textarea()
                 self.fields["equipments"].widget = Textarea()
                 self.fields["lights"].widget = Textarea()
    
           def clean_actors(self):
                 data = self.cleaned_data.get('actors')
                 print 'cleaning actors'
                 return [data]
    
    
          class Meta:
                model = MovieModel
    
    =============================================
    views.py
    
    """ This will display the form used to create a MovieModel """
    
    from django.shortcuts import render_to_response
    from django.template import RequestContext
    from forms import MovieModelForm
    
    def add_movie(request):
           if request.method == "POST":
                 form = MovieModelForm(request.POST)
    
                 if form.is_valid():
                        new_moviemodel = form.save()
                        return HttpResponseRedirect('/data/')
    
           else:
                 form = MovieModelForm()
    
           return render_to_response('add_movie_form.html', {form:form,}, context_instance=RequestContext(request))
    
  • sharkfin
    sharkfin about 13 years
    Thanks for the input. The clean_actors(self) doesn't seem to be called either though. The input can be anything.
  • Yuji 'Tomita' Tomita
    Yuji 'Tomita' Tomita about 13 years
    With a second look at the code, it looks like you are not calling the superclass __init__ - that should trigger an AttributeError saying either fields doesn't exist or actors doesn't exist. Very odd. : )
  • sharkfin
    sharkfin about 13 years
    Oh oops, I must have deleted that when I was editing my post. Otherwise you're right: 'self.fields' would have thrown some error. But the clean problem still persists. It's not being called. I printed out the form, and it looked something like this: [(u'actors', u'testing actors'), ..... ]
  • Yuji 'Tomita' Tomita
    Yuji 'Tomita' Tomita about 13 years
    It's not being called at all? That's odd. I just tested on a fresh ModelForm, and clean_FIELD does get called. Not sure what to say...
  • sharkfin
    sharkfin about 13 years
    Hmm, I'm going to redo this and see what happens. I'll let you know (I have to get ready for something now, blarg). Thank you for the suggestions so far!
  • sharkfin
    sharkfin about 13 years
    I'll try this and let you know! Thanks!
  • Josh Smeaton
    Josh Smeaton about 13 years
    @sharkfin, see my final update. If you're using django 1.3 (you should be for a new project!), the last edit should be the cleanest way to achieve what you want.
  • sharkfin
    sharkfin about 13 years
    Thanks, Josh. I'm able to see clean() being called and the value being passed in, but now it tells me clean() takes exactly 2 arguments (3 given)
  • sharkfin
    sharkfin about 13 years
    This will fix the error: return super(ModelCommaSeparatedChoiceField, self).clean(value)
  • Josh Smeaton
    Josh Smeaton about 13 years
    @shark whoops yeah thanks for the edit. So did this method work for you?
  • sharkfin
    sharkfin about 13 years
    Josh's post seems to work, but clean_actors() still isn't being called for the ModelForm. I want to add the text to the database if it's not already in it. Any reason why this is?
  • sharkfin
    sharkfin about 13 years
    With your updated code, I'm trying to use clean_actors() to add the actor text if it's not already in the actor database table. clean_actors() (inside MovieModelForm) never seems to be called though?
  • Josh Smeaton
    Josh Smeaton about 13 years
    Nope, that's not going to work. Inside the ModelMultipleChoiceField, it validates that the values already exist, and throws an error if they don't. This is done in the field clean() method, not the form clean method. It will never make it through to the form. Look into the source code of django.forms.models.ModelMultipleChoiceField, that will give you a much clearer picture. I don't think it's a good idea to allow users to enter so many instances in that way.. you really would be better off keeping a separate form for adding actors/lights/equipment independently. My 2 cents.
  • Yuji 'Tomita' Tomita
    Yuji 'Tomita' Tomita about 13 years
    Why clean_actors isn't being called - I have no idea. I can not reproduce this on django 1.3. I created a fresh ModelForm for a model with an m2m field, defined nothing but a clean_method and an __init__ widget override as you have done, and the clean methods are called.