updating formsets with FormView CBV

I am trying to update several instances of a model via formsets. I have a unique constraint on a field in the model.

models

class Pairs(models.Model):
    keyword = models.CharField(unique=True, max_length=255)
    state = models.BooleanField(default=False)

forms

PairsFormSet = forms.modelformset_factory(
    Pairs,
    fields="__all__",
    form=PairsForm, # nothing serious here, just labels
    extra=2,
    min_num=1,
    can_delete=True,
)

views

class ManagePairs(FormView):
    form_class = PairsFormSet
    model = Pairs
    template_name = "pairs.html"
    success_url = reverse_lazy("app:template")

    def form_valid(self, form):
        """If the form is valid, redirect to the supplied URL."""

        form.save()
        return super().form_valid(form)

    def form_invalid(self, form):
        """If the form is invalid, render the invalid form."""

        print(form.errors)
        return super().form_invalid(form)

OBSERVATION:
It works for the first few entries (allowing me to add new Pairs … up to a thrid instance). The form starts throwing errors after the second or third instance:

output from form_invalid:
[{'id': ['This field is required.'], 'keyword': ['Pair with this Keyword already exists.']}, {'id': ['This field is required.'], 'keyword': ['Pair with this Keyword already exists.']}, {'id': ['This field is required.'], 'keyword': ['Pair with this Keyword already exists.']}, {}, {}]

It appears the formset is attempting to create duplicate instances rather than adding only newer unique instances. Those forms did not change, why is it bothering with them?

Are you allowing the user to add additional instances of the form for the formset using JavaScript in the browser? If so, please post that JavaScript.

No JavaScript. Just plain old HTML/CSS form

What does your template look like?

What does your PairsForm look like?

Are you supplying any url parameters to that view?

PairsForm

class PairsForm(forms.ModelForm):
    class Meta:
        model = Pairs
        fields = "__all__"
        labels = {
            "keyword": "Key",
            "state": "Value",
        }
        widgets = {
            "keyword": forms.TextInput(),
            "state": forms.CheckboxInput(),
        }

Template:

{% extends "common.html" %}
{% block main %}
    <h3 class="center">Headings</h3>
    <form method='POST'>
        {% csrf_token %}
        {{ form.management_form }}
        <h4 class="margin-t10px">Keys and Associated States</h4>
        <div class="flex-wrap gap10px margin-t10px">
        {% for pairform in form %}
            <div class="widgets-tag">
                <div class="widgets margin">
                    <div class="widget widget-text">
                        <label>Keyword</label>
                        {{ pairform.keyword }}
                    </div>
                    <div class="widget widget-check">
                        <label>State</label>
                        {{ pairform.state }}
                    </div>
                    <div class="widget widget-check">
                        <label>Delete</label>
                        {{ pairform.DELETE }}
                    </div>
                </div>  
            </div> 
        {% endfor %}
        </div>      
        <div class="buttons margin-t20px">
            <a href="{% url 'app:anotherpage' %}" class="button">
                <div class="button-inner">Next Page</div>
            </a>
            <button class="button">
                <div class="button-inner">More Pairs</div>
            </button>
            <button class="button-do">
                <div class="button-inner-do">Save</div>
            </button>
        </div>
    </form>
{% endblock %}

See my last post above

So it appears that one of the issues is going to be that you have in your formset definition:

But the form you are rendering only contains the keyword and state fields. You are not including the id field in the form.

I’m guessing you don’t want to manually set that field, so you want to change that setting to only include the two fields you want included.

(Note: There may be other issues here - but this is the first one I see based on the error messages you’re reporting above.)

You are right. I don’t want to be setting the ids but it appears I have to do so. My options are limited. Everything works when I drop {{ form }} on the template. That approach manages the id nicely. Unfortunately, that approach limits custom styling to some degree. Setting the fields argument doesn’t fix the issue. It does look like I have to manage the ids on the template.

Therefore, given the following id input line (as example)
<input type="hidden" name="form-0-id" value="2" id="id_form-0-id">

The index in the above example is “0” while formset.form.id is 2. Do the forms in a formset store their own indexes or do I have to depend on the for-loop counter? If the form objects have index attributes, how do I access them?

Figured it out!

{{ form.id }} produces that line. It is not the numeric value for the id as I thought. It is actually input html tag … the whole line that I was looking to reconstruct

The fix:

{% for pairform in form %}
            <div class="widgets-tag">
                <div class="widgets margin">
                    <div class="widget widget-text">
                        <label>Keyword</label>
                        {{ pairform.keyword }}
                    </div>
                    <div class="widget widget-check">
                        <label>State</label>
                        {{ pairform.state }}
                    </div>
                    <div class="widget widget-check">
                        <label>Delete</label>
                        {{ pairform.DELETE }}
                    </div>
                   {{ pairform.id }}  <!-- the fix here -->
                </div>  
            </div> 
        {% endfor %}
1 Like