Login

Arbitrary length formset

Author:
Rupe
Posted:
September 1, 2009
Language:
Python
Version:
1.1
Tags:
form formset factory arbitrary-length
Score:
1 (after 1 ratings)

A formset class where you can add forms as you discover the need within your code. There is also the ability to add ManagmentForm fields.

If you ever found yourself in a situation where 1) you have repeated forms that need to be displayed in different locations, or 2) if you find the application logic works better if you add forms as you discover you need them, this code will help you out.

Below is pseudo code based on a real implementation I used. Each form had a save button and the SELECTED_PAYMENT field was set through JavaScript. It is very difficult to use JavaScript with repeated forms, without using a formset.

from myProject.myApp import myFormsUtils
from myProject.myApp.forms import PaymentForm

SELECTED_PAYMENT = 'SELECTED_PAYMENT'

# extra_fields format: {Field name: (Field type, Initial value)}
l_extra_fields = {SELECTED_PAYMENT: (forms.IntegerField, -1)}

PaymentFormSetType = myFormsUtils.formset_factory(PaymentForm, extra=0, extra_fields=l_extra_fields)

if request.method == 'POST':
    paymentFormSet = PaymentFormSetType(data=request.POST)
    if paymentFormSet.is_valid():
        li_curFormIdx = pagaFormSet.management_form.cleaned_data[SELECTED_PAYMENT]

        paymntForm = paymentFormSet.forms[li_curFormIdx]

        ... do stuff ...

# To generate the formset
paymentFormSet = PagamentoFormSetType()

# You can re-add a form retrieved (as in the one above)
l_form = paymentFormSet.add_form(paymntForm)
# Or use the add function just like creating a new form
l_form = paymentFormSet.add_form(personID=argPersonID, propID=argPropID, year=argYr, amt=lc_Amt)

I then stored the l_form variables above directly into a unique Context structure and displayed them each individually in my template. Of course this also meant that I also had to output the paymentFormSet.management_form explicitly within my template.

EDIT 09-11-2009: Modified the initial_form_count() method to properly handle initial form values in conjunction with dynamically added forms.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from django import forms

from django import forms
from django.forms.util import ErrorDict, ErrorList
from django.utils.encoding import force_unicode, smart_unicode
from django.utils.safestring import mark_safe

class Form(forms.Form):

    def add_error(self, field_name, message):
        if field_name is None: field_name = forms.NON_FIELD_ERRORS
        # self.errors is checked first because it triggers self.clean_all
        # but only if self._errors is None
        if self.errors is None:
            self._errors = ErrorDict()
        message = smart_unicode(message)
        messages = ErrorList([message])
        if not self._errors.has_key(field_name):
            self._errors[field_name] = messages
        else:
            self._errors[field_name].extend(messages)

    def all_errors(self):
        if self.errors == {}: return None
        self.errors['%sALL' % forms.NON_FIELD_ERRORS] = ErrorList()
        for k in self.errors:
            self.errors['%sALL' % forms.NON_FIELD_ERRORS].extend(self.errors[k])
        return self.errors.get('%sALL' % forms.NON_FIELD_ERRORS, self.error_class())


class XManagementForm(forms.formsets.ManagementForm):
    """
    ``ManagementForm`` is used to keep track of how many form instances
    are displayed on the page. If adding new forms via javascript, you should
    increment the count field of this form as well.
    """
    def __init__(self, data=None, extra_fields=None, *args, **kwargs):
        for f, tv in extra_fields.items():
            self.base_fields[f] = tv[0](widget=forms.HiddenInput)
            if kwargs.has_key('initial'):
                kwargs['initial'][f] = tv[1]
        super(XManagementForm, self).__init__(data, *args, **kwargs)


class BaseXFormSet(forms.formsets.BaseFormSet):

    def __init__(self, *args, **kwargs):
        self.added_forms = 0
        super(BaseXFormSet, self).__init__(*args, **kwargs)

    def _management_form(self):
        """Returns the ManagementForm instance for this FormSet."""
        if self.data or self.files:
            form = XManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix,
                extra_fields=self.extra_fields)
            #import pdb; pdb.set_trace()
            if not form.is_valid():
                raise forms.ValidationError('ManagementForm data is missing or has been tampered with')
        else:
            form = XManagementForm(auto_id=self.auto_id, prefix=self.prefix, extra_fields=self.extra_fields,
                initial={
                    forms.formsets.TOTAL_FORM_COUNT: self.total_form_count(),
                    forms.formsets.INITIAL_FORM_COUNT: self.initial_form_count()
            })
        return form
    management_form = property(_management_form)

    def initial_form_count(self):
        """Returns the number of forms that are required/pre-populated in this FormSet."""
        if not (self.data or self.files):
            initial_forms = self.initial and len(self.initial) or 0
            initial_forms += self.added_forms
            if initial_forms > self.max_num > 0:
                initial_forms = self.max_num
            return initial_forms
        return super(BaseXFormSet, self).initial_form_count()

    def add_form(self, existing_form=None, **kwargs):
        self.added_forms = self.added_forms + 1
        l_curIdx = len(self.forms)
        if existing_form:
            l_new_form = existing_form
            l_new_form.prefix = self.add_prefix(l_curIdx)
        else:
            l_new_form = self._construct_form(l_curIdx, **kwargs)
        l_new_form.form_index = l_curIdx
        self.forms.append(l_new_form)
        return l_new_form

def formset_factory(form, formset=BaseXFormSet, extra=0, can_order=False,
                    can_delete=False, max_num=0, extra_fields=None):
    """Return a FormSet for the given form class."""
    attrs = {'form': form, 'extra': extra,
             'can_order': can_order, 'can_delete': can_delete,
             'max_num': max_num, 'extra_fields': extra_fields}
    return type(form.__name__ + 'FormSet', (formset,), attrs)

More like this

  1. Dynamically adding forms to a formset with jQuery by elo80ka 6 years ago
  2. Complex Formsets, Redux by smagala 5 years ago
  3. grouper tag by visgean 3 years, 7 months ago
  4. Complex Form Preview by smagala 6 years ago
  5. Complex Formsets by smagala 6 years, 2 months ago

Comments

Please login first before commenting.