-->

Django formset unit test

2019-02-03 01:10发布

问题:

I can't running 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': 'new@gmail.com', 
            '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')

回答1:

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.



回答2:

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 
}


回答3:

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.


回答4:

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.



回答5:

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