Django formset unit test
Asked Answered
S

7

31

I can't run a unit test with formset.

I try to do a test:

class NewClientTestCase(TestCase):
    
    def setUp(self):
        self.c = Client()

    def test_0_create_individual_with_same_adress(self):
        
        post_data =  {
            'ctype': User.CONTACT_INDIVIDUAL,
            'username': 'dupond.f',        
            'email': '[email protected]', 
            'password': 'pwd', 
            'password2': 'pwd', 
            'civility': User.CIVILITY_MISTER, 
            'first_name': 'François', 
            'last_name': 'DUPOND', 
            'phone': '+33 1 34 12 52 30', 
            'gsm': '+33 6 34 12 52 30', 
            'fax': '+33 1 34 12 52 30', 
            'form-0-address1': '33 avenue Gambetta', 
            'form-0-address2': 'apt 50', 
            'form-0-zip_code': '75020', 
            'form-0-city': 'Paris', 
            'form-0-country': 'FRA', 
            'same_for_billing': True,            
        }
        
        response = self.c.post(reverse('client:full_account'), post_data, follow=True)   

        self.assertRedirects(response, '%s?created=1' % reverse('client:dashboard'))

and I have this error:

ValidationError: [u'ManagementForm data is missing or has been tampered with']

My view :

def full_account(request, url_redirect=''):    
    from forms import NewUserFullForm,  AddressForm,  BaseArticleFormSet
    
    fields_required = []
    fields_notrequired = []
    
    AddressFormSet = formset_factory(AddressForm, extra=2,  formset=BaseArticleFormSet)
    
    if request.method == 'POST':        
        form = NewUserFullForm(request.POST)        
        objforms = AddressFormSet(request.POST)            
       
        if objforms.is_valid() and form.is_valid():            
            user = form.save()            
            address = objforms.forms[0].save()

            
            if url_redirect=='':
                url_redirect = '%s?created=1' % reverse('client:dashboard')
                logon(request, form.instance)            
            return HttpResponseRedirect(url_redirect)
    else:
        form = NewUserFullForm()
        objforms = AddressFormSet()   
    
    return direct_to_template(request, 'clients/full_account.html', {
        'form':form,
        'formset': objforms, 
        'tld_fr':False, 
    })

and my form file :

class BaseArticleFormSet(BaseFormSet):

    def clean(self):        
        
        msg_err = _('Ce champ est obligatoire.')
        non_errors = True
        
        if 'same_for_billing' in self.data and self.data['same_for_billing'] == 'on':
            same_for_billing = True
        else:            
            same_for_billing = False
        
        for i in [0, 1]:
            
            form = self.forms[i]           
            
            for field in form.fields:                                
                name_field = 'form-%d-%s' % (i, field )
                value_field = self.data[name_field].strip()                
                
                if i == 0 and self.forms[0].fields[field].required and value_field =='':                    
                    form.errors[field] = msg_err                    
                    non_errors = False
                    
                elif i == 1 and not same_for_billing and self.forms[1].fields[field].required and value_field =='':
                    form.errors[field] = msg_err                    
                    non_errors = False
        
        return non_errors

class AddressForm(forms.ModelForm):

    class Meta:
        model = Address

    address1 = forms.CharField()
    address2 = forms.CharField(required=False)
    zip_code = forms.CharField()
    city = forms.CharField()
    country = forms.ChoiceField(choices=CountryField.COUNTRIES,  initial='FRA')
Satisfy answered 27/10, 2009 at 13:19 Comment(0)
G
14

Every Django formset comes with a management form that needs to be included in the post. The official docs explain it pretty well. To use it within your unit test, you either need to write it out yourself. (The link I provided shows an example), or call formset.management_form which outputs the data.

Ganja answered 27/10, 2009 at 13:23 Comment(1)
Didn't work for me. Does anybody have an example? I don't know what I could be doing wrong.Myrmecology
H
30

In particular, I've found that the ManagmentForm validator is looking for the following items to be POSTed:

form_data = {
            'form-TOTAL_FORMS': 1, 
            'form-INITIAL_FORMS': 0 
}
Historical answered 14/12, 2009 at 0:29 Comment(4)
django = "==3.0.*" doesn't work for me. Response return "ManagementForm data is.." validation error, but form.errors in pdb eq to {'TOTAL_FORMS': ['.. required.'], 'INITIAL_FORMS': ['.. required.']}. Tried to post without form- prefix - didn't work.Yulandayule
wow this should be in the official doc. Add these 2 key:value pair to data thats being posted from testclient.Dryden
docs.djangoproject.com/en/3.1/topics/forms/formsets/…Untangle
Thanks @Historical :) note that if testing POSTing a single form it is essential that 'form-INITIAL_FORMS is 0 & form-TOTAL_FORMS is 1. In my case I was blocked because I had set both to 1 & was blocked by id is requiredEudo
G
14

Every Django formset comes with a management form that needs to be included in the post. The official docs explain it pretty well. To use it within your unit test, you either need to write it out yourself. (The link I provided shows an example), or call formset.management_form which outputs the data.

Ganja answered 27/10, 2009 at 13:23 Comment(1)
Didn't work for me. Does anybody have an example? I don't know what I could be doing wrong.Myrmecology
N
8

It is in fact easy to reproduce whatever is in the formset by inspecting the context of the response.

Consider the code below (with self.client being a regular test client):

url = "some_url"

response = self.client.get(url)
self.assertEqual(response.status_code, 200)

# data will receive all the forms field names
# key will be the field name (as "formx-fieldname"), value will be the string representation.
data = {}

# global information, some additional fields may go there
data['csrf_token'] = response.context['csrf_token']

# management form information, needed because of the formset
management_form = response.context['form'].management_form
for i in 'TOTAL_FORMS', 'INITIAL_FORMS', 'MIN_NUM_FORMS', 'MAX_NUM_FORMS':
    data['%s-%s' % (management_form.prefix, i)] = management_form[i].value()

for i in range(response.context['form'].total_form_count()):
    # get form index 'i'
    current_form = response.context['form'].forms[i]

    # retrieve all the fields
    for field_name in current_form.fields:
        value = current_form[field_name].value()
        data['%s-%s' % (current_form.prefix, field_name)] = value if value is not None else ''

# flush out to stdout
print '#' * 30
for i in sorted(data.keys()):
    print i, '\t:', data[i]

# post the request without any change
response = self.client.post(url, data)

Important note

If you modify data prior to calling the self.client.post, you are likely mutating the DB. As a consequence, subsequent call to self.client.get might not yield to the same data, in particular for the management form and the order of the forms in the formset (because they can be ordered differently, depending on the underlying queryset). This means that

  • if you modify data[form-3-somefield] and call self.client.get, this same field might appear in say data[form-8-somefield],
  • if you modify data prior to a self.client.post, you cannot call self.client.post again with the same data: you have to call a self.client.get and reconstruct data again.
Nutria answered 20/7, 2016 at 11:11 Comment(0)
S
3

Django formset unit test

You can add following test helper methods to your test class [Python 3 code]

def build_formset_form_data(self, form_number, **data):
    form = {}
    for key, value in data.items():
        form_key = f"form-{form_number}-{key}"
        form[form_key] = value
    return form

def build_formset_data(self, forms, **common_data):
    formset_dict = {
        "form-TOTAL_FORMS": f"{len(forms)}",
        "form-MAX_NUM_FORMS": "1000",
        "form-INITIAL_FORMS": "1"
    }
    formset_dict.update(common_data)
    for i, form_data in enumerate(forms):
        form_dict = self.build_formset_form_data(form_number=i, **form_data)
        formset_dict.update(form_dict)
    return formset_dict

And use them in test

def test_django_formset_post(self):
    forms = [{"key1": "value1", "key2": "value2"}, {"key100": "value100"}]
    payload = self.build_formset_data(forms=forms, global_param=100)
    print(payload)
    # self.client.post(url=url, data=payload)

You will get correct payload which makes Django ManagementForm happy

{
    "form-INITIAL_FORMS": "1",
    "form-TOTAL_FORMS": "2",
    "form-MAX_NUM_FORMS": "1000",
    "global_param": 100,
    "form-0-key1": "value1",
    "form-0-key2": "value2",
    "form-1-key100": "value100",
}

Profit

Stadiometer answered 5/7, 2020 at 18:41 Comment(0)
U
1

There are several very useful answers here, e.g. pymen's and Raffi's, that show how to construct properly formatted payload for a formset post using the test client.

However, all of them still require at least some hand-coding of prefixes, dealing with existing objects, etc., which is not ideal.

As an alternative, we could create the payload for a post() using the response obtained from a get() request:

def create_formset_post_data(response, new_form_data=None):
    if new_form_data is None:
        new_form_data = []
    csrf_token = response.context['csrf_token']
    formset = response.context['formset']
    prefix_template = formset.empty_form.prefix  # default is 'form-__prefix__'
    # extract initial formset data
    management_form_data = formset.management_form.initial
    form_data_list = formset.initial  # this is a list of dict objects
    # add new form data and update management form data
    form_data_list.extend(new_form_data)
    management_form_data['TOTAL_FORMS'] = len(form_data_list)
    # initialize the post data dict...
    post_data = dict(csrf_token=csrf_token)
    # add properly prefixed management form fields
    for key, value in management_form_data.items():
        prefix = prefix_template.replace('__prefix__', '')
        post_data[prefix + key] = value
    # add properly prefixed data form fields
    for index, form_data in enumerate(form_data_list):
        for key, value in form_data.items():
            prefix = prefix_template.replace('__prefix__', f'{index}-')
            post_data[prefix + key] = value
    return post_data

The output (post_data) will also include form fields for any existing objects.

Here's how you might use this in a Django TestCase:

def test_post_formset_data(self):
    url_path = '/my/post/url/'
    user = User.objects.create()
    self.client.force_login(user)
    # first GET the form content
    response = self.client.get(url_path)
    self.assertEqual(HTTPStatus.OK, response.status_code)
    # specify form data for test
    test_data = [
        dict(first_name='someone', email='[email protected]', ...),
        ...
    ]
    # convert test_data to properly formatted dict
    post_data = create_formset_post_data(response, new_form_data=test_data)
    # now POST the data
    response = self.client.post(url_path, data=post_data, follow=True)
    # some assertions here
    ...

Some notes:

  • Instead of using the 'TOTAL_FORMS' string literal, we could import TOTAL_FORM_COUNT from django.forms.formsets, but that does not seem to be public (at least in Django 2.2).

  • Also note that the formset adds a 'DELETE' field to each form if can_delete is True. To test deletion of existing items, you can do something like this in your test:

      ...
      post_data = create_formset_post_data(response)
      post_data['form-0-DELETE'] = True
      # then POST, etc.
      ...
    
  • From the source, we can see that there is no need include MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT in our test data:

    MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of the management form, but only for the convenience of client-side code. The POST value of them returned from the client is not checked.

Untangle answered 14/10, 2020 at 13:50 Comment(0)
C
0

This doesn't seem to be a formset at all. Formsets will always have some sort of prefix on every POSTed value, as well as the ManagementForm that Bartek mentions. It might have helped if you posted the code of the view you're trying to test, and the form/formset it uses.

Claussen answered 27/10, 2009 at 13:45 Comment(2)
I edited my question and added the contents of my files view and formSatisfy
I see you've updated the POST values so they do include the prefix - now the only thing missing is the management form data.Claussen
A
0

My case may be an outlier, but some instances were actually missing a field set in the stock "contrib" admin form/template leading to the error

"ManagementForm data is missing or has been tampered with"

when saved.

The issue was with the unicode method (SomeModel: [Bad Unicode data]) which I found investigating the inlines that were missing.

The lesson learned is to not use the MS Character Map, I guess. My issue was with vulgar fractions (¼, ½, ¾), but I'd assume it could occur many different ways. For special characters, copying/pasting from the w3 utf-8 page fixed it.

postscript-utf-8

Anatole answered 16/6, 2010 at 13:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.