Error when creating formset from POST data in test

Context: I’m building a basic bookkeeping application, just updated to Django 5.0.1.

I have a complicated custom CreateView which render a form for an Entry as well as the rows in that entry, EntryRows. The post method of this view has the following code snippet:

    entryform = EntryForm(request.POST)
    entryrow_formset = EntryRowFormSet(request.POST)

    if entryform.is_valid() and entryrow_formset.is_valid():
        entry = entryform.save()

        entryrow_instances = entryrow_formset.save(commit = False)

This works perfectly!

I built a test for this view. I used the view to trigger this post method, logged the request.POST and copied the whole thing to be hardcoded in my test. However, the following snippet

    data = {
        'journal': ['journ1'], 'notes': [''], 'form-TOTAL_FORMS': ['2'],
        'form-INITIAL_FORMS': ['0'], 'form-MIN_NUM_FORMS': ['0'], 'form-MAX_NUM_FORMS': ['1000'],
        'form-0-id': [''], 'form-0-date': ['2023-12-27'], 'initial-form-0-date': ['2023-12-27', '', ''],
        'form-0-ledger': ['test1'], 'form-0-account': ['acc1'], 'form-0-value': ['2'], 'form-1-id': [''],
        'form-1-DELETE': [''], 'form-1-date': ['2023-12-27'], 'form-1-ledger': ['test2'],
        'form-1-account': ['acc2'], 'form-1-value': ['-3']
    }

    formset = EntryRowFormSet(data)
    print(formset.is_valid())

    print(formset.non_form_errors())

Bafflingly, this formset always yields the error: ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS, form-MIN_NUM_FORMS, form-MAX_NUM_FORMS. You may need to file a bug report if the issue persists.

Those fields are clearly in the data right? Is this a bug in Django?

For reference, here is the code for the EntryRowForm and the EntryRowBaseFormSet:

The EntryRowFormSet is constructed using the modelformset_factory with default settings.

Thanks in advance for any help!

I don’t believe those values are submitted as lists. (You can’t trust that a print function is going to give you an accurate representation of the internal structure of an object.)

If you’re supplying form data for a form, you supply it as “key: value”, not “key: [value]”. See the examples at The Forms API.
(Yes, it’s possible that it works for some other values in forms, but it doesn’t appear to work in the management form.)

Thanks for the quick reply!

You were spot on; even though the request.POST got printed as the snippet below, the actual data should have just been passed as strings, not lists of strings.

<QueryDict: {'journal': ['journ1'], 'notes': [''], 'form-TOTAL_FORMS': ['2'], 'form-INITIAL_FORMS': ['0'], 'form-MIN_NUM_FORMS': ['0'], 'form-MAX_NUM_FORMS': ['1000'], 'form-0-id': [''], 'form-0-date': ['2023-12-27'], 'initial-form-0-date': ['2023-12-27'], 'form-0-ledger': ['test1'], 'form-0-account': ['acc1'], 'form-0-value': ['2'], 'form-1-id': [''], 'form-1-DELETE': [''], 'form-1-date': ['2023-12-27'], 'form-1-ledger': ['test2'], 'form-1-account': ['acc2'], 'form-1-value': ['-3']}>

Also for completeness, here’s what data should look like:

data = {
    'journal': 'journ1', 'notes': '', 'form-TOTAL_FORMS': '2',
    'form-INITIAL_FORMS': '0', 'form-MIN_NUM_FORMS': '0', 'form-MAX_NUM_FORMS': '1000',
    'form-0-id': '', 'form-0-date': '2023-12-27', 'initial-form-0-date': '2023-12-27',
    'form-0-ledger': 'test1', 'form-0-account': 'acc1', 'form-0-value': '2', 'form-1-id': '',
    'form-1-DELETE': '', 'form-1-date': '2023-12-27', 'form-1-ledger': 'test2',
    'form-1-account': 'acc2', 'form-1-value': '-3'
}

Note that the key initial-form-0-date had two empty strings in its value “list”, I removed those as well.

For complete completeness, I also have the following test:

def test_entries_create(self):
    data = {
        'journal': ['journ1'], 'notes': [''], 'form-TOTAL_FORMS': ['3'],
            'form-INITIAL_FORMS': ['0'], 'form-MIN_NUM_FORMS': ['0'], 'form-MAX_NUM_FORMS': ['1000'],
            'form-0-id': [''], 'form-0-date': ['2023-12-27'], 'initial-form-0-date': ['2023-12-27', '', ''],
            'form-0-ledger': ['test1'], 'form-0-account': ['acc1'], 'form-0-value': ['69'], 'form-1-id': [''],
            'form-1-DELETE': [''], 'form-1-date': ['2023-12-27'], 'form-1-ledger': ['test2'],
            'form-1-account': ['acc2'], 'form-1-value': ['-60'], 'form-2-id': [''], 'form-2-DELETE': [''],
            'form-2-date': ['2023-12-27'], 'form-2-ledger': ['test2'], 'form-2-account': ['acc1'],
            'form-2-value': ['-9']
    }

    response = self.client.post(reverse("entries:create"), data)

    self.assertEqual(response.status_code, 302)
    self.assertEqual(Entry.objects.count(), 1)
    self.assertEqual(EntryRow.objects.count(), 3)

This passes just fine. The difference is that here, the dict is passed as data to the POST request, whereas in the test in my original post, the data got passed to the formset. The latter doesn’t enjoy the lists.