Luis Silva
Luis Silva

Reputation: 173

How to fix the error " 'id': Select a valid choice" on Modelformset validation?

I have a Modelformset that raises this validation error on submission:

{'id': ['Select a valid choice. That choice is not one of the available choices.']}

This error appears the same number of times as objects in my queryset, which is:

qs = Task.objects.filter(property=property)

Over the last few days I have been trying to fix this. I've read a lot of other similar posts and tried different solutions but none of them worked for me.

My formset can be seen here:

def add_taskcheck(request, property_pk, pk):
    property = get_object_or_404(Property, pk=property_pk)
    pcheck = get_object_or_404(Propertycheck, pk=pk)
    qs = Task.objects.filter(property=property)
    tasks = Task.objects.filter(property=property_pk)

    TaskCheckFormset = modelformset_factory(TaskCheck, form=TaskCheckForm, fields=('status','image','notes'), extra=0)
    if request.method == 'POST':
        formset = TaskCheckFormset(request.POST, request.FILES, queryset=qs)
        print(formset.errors)
        if formset.is_valid():
            taskcheck = formset.save(commit=False)
            taskcheck.property_check=pcheck.id
            return HttpResponseRedirect(reverse('propertycheck:details', args=[pk]))
    else:
        formset = TaskCheckFormset(queryset=qs)

    context = {
        'title':"Add Property Check",
        'task':tasks,
        'reference':property_pk,
        'formset':formset,
    }
    return render(request, 'propertycheck/add-taskcheck.html', context)

And my form:

class TaskCheckForm(forms.ModelForm):
    status = forms.ModelChoiceField(queryset=TaskStatus.objects.all(), to_field_name="name", widget=forms.Select(attrs={
    'class':'form-control custom-select',
    'id':'type',
    }))
    image = ...
    notes = ...

    class Meta:
        model = TaskCheck
        fields = ('status','image','notes')

And finally my models:

class TaskCheck(models.Model):
    status = models.ForeignKey(TaskStatus)
    image = models.ImageField(upload_to='task_check', blank=True, null=True)
    notes = models.TextField(max_length=500, blank=True)
    task = models.ForeignKey(Task)
    property_check = models.ForeignKey(Propertycheck)

class Task(models.Model):
    task = models.CharField(max_length=100, unique=True)
    category = models.ForeignKey(Categories)
    property = models.ManyToManyField(Property)

I already know that the problem is not related to 'status' field. Actually I believe that this is related to the 'task' field. I've also added the {{ form.id }} to template, as I've seen on some other questions.

For reference, my template:

{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
<div class="form-group">
      <h5 class="card-title">{{ form.instance.task }}</h5>
    <div class="row">
        <div class="col-md-3">
            <label for="pname">Status</label>
            {{ form.status }}
            {{ form.status.errors }}
        </div>
        <div class="col-md-3">
            <label for="pname">Image</label>
            {{ form.image }}
            {{ form.image.errors }}
        </div>
        <div class="col-md-3">
            <label for="pname">Notes</label>
            {{ form.notes }}
            {{ form.notes.errors }}
        </div>
    </div>
</div>
{% endfor %}

So what am I doing wrong?

--------UPDATE------------

From Oleg's answer I changed my formset validation.

if request.method == 'POST':
        formset = TaskCheckFormset(request.POST, request.FILES, queryset=qs)
        if formset.is_valid():
            instances = formset.save(commit=False)
            for instance in instances:
                # do something with instance
                instance.property_check=pcheck.id
                instance.save()

Upvotes: 3

Views: 733

Answers (3)

Muhammad Tahir
Muhammad Tahir

Reputation: 434

The issue raised because of different models used in ModelFormSet and QuerySet. The issues can be solved by using:

property = get_object_or_404(Property, pk=property_pk)
pcheck = get_object_or_404(Propertycheck, pk=pk)
qs = Task.objects.filter(property=property)
category = qs.values('category').distinct()
TaskCheckFormset = formset_factory(TaskCheckForm,extra=len(qs))

formset = TaskCheckFormset()
    for i in range(len(qs)):
        formset.forms[i].initial['task']=qs[i].id
        formset.forms[i].instance.task=qs[i]
        formset.forms[i].instance.property_check=pcheck
        formset.forms[i].initial['property_check']=pcheck.id

Upvotes: 2

Ijharul Islam
Ijharul Islam

Reputation: 1487

Looks like your query is not correct. You are doing

qs = Task.objects.filter(property=property)

It should be like this-

qs = Task.objects.filter(property__id=property.id)

Or you can do like this:

qs = Task.objects.filter(property__in=[property_pk])

Here property is a many to many field. Your query looks like searching foreign key.

Upvotes: 0

Oleg Russkin
Oleg Russkin

Reputation: 4412

This error points to invalid choice in ModelChoiceField that in provided example is status field of TaskCheckForm.

This is class level attribute and is initiated only once application starts and TaskCheckForm is being imported for the first time. And its QuerySet is resolved only once at start - and it will see present at that time TaskStatus objects and never update its choices list for new or deleted items.

To handle relationship fields and other with dynamic queryset recommended way can be used - define empty QuerySet on field and set it to required one in form's __init__ method:

    status = forms.ModelChoiceField(queryset=TaskStatus.objects.none(), to_field_name="name", widget=...)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['status'].queryset = TaskStatus.objects.all()

Other potential problem places in code:

  • tasks = Task.objects.filter(property=property_pk) - will return a list of results. But later in code is assigned to task variable in template which may expect (but may be it is ok and it expects list) single item. You can use tasks = Task.objects.filter(property=property_pk).first() instead.

  • taskcheck = formset.save(commit=False) - first, it returns a list of items (because it is formset to act on a set of forms), so in order to add property_check attribute to items you need to iterate over result like in example; second - commit=False means instances will not be saved, which is ok as some additional attribute is set later, but no instance.save() is called afterwards - so still no changes will be saved.

Upvotes: 1

Related Questions