summaryrefslogtreecommitdiff
path: root/django/newforms/formsets.py
blob: 56179a9b391793daef15ea463d91741880134fb7 (plain)
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
from forms import Form
from fields import IntegerField, BooleanField
from widgets import HiddenInput, Media
from util import ErrorList, ValidationError

__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')

# special field names
FORM_COUNT_FIELD_NAME = 'COUNT'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'

class ManagementForm(Form):
    """
    ``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, *args, **kwargs):
        self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
        super(ManagementForm, self).__init__(*args, **kwargs)

class BaseFormSet(object):
    """A collection of instances of the same Form class."""

    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
            initial=None, error_class=ErrorList):
        self.is_bound = data is not None or files is not None
        self.prefix = prefix or 'form'
        self.auto_id = auto_id
        self.data = data
        self.files = files
        self.initial = initial
        self.error_class = error_class
        # initialization is different depending on whether we recieved data, initial, or nothing
        if data or files:
            self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
            if self.management_form.is_valid():
                self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME]
                self.required_forms = self.total_forms - self.num_extra
                self.change_form_count = self.total_forms - self.num_extra
            else:
                # not sure that ValidationError is the best thing to raise here
                raise ValidationError('ManagementForm data is missing or has been tampered with')
        elif initial:
            self.change_form_count = len(initial)
            self.required_forms = len(initial)
            self.total_forms = self.required_forms + self.num_extra
            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
        else:
            self.change_form_count = 0
            self.required_forms = 0
            self.total_forms = self.num_extra
            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)

    def _get_add_forms(self):
        """Return a list of all the add forms in this ``FormSet``."""
        FormClass = self.form_class
        if not hasattr(self, '_add_forms'):
            add_forms = []
            for i in range(self.change_form_count, self.total_forms):
                kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
                if self.data:
                    kwargs['data'] = self.data
                if self.files:
                    kwargs['files'] = self.files
                add_form = FormClass(**kwargs)
                self.add_fields(add_form, i)
                add_forms.append(add_form)
            self._add_forms = add_forms
        return self._add_forms
    add_forms = property(_get_add_forms)

    def _get_change_forms(self):
        """Return a list of all the change forms in this ``FormSet``."""
        FormClass = self.form_class
        if not hasattr(self, '_change_forms'):
            change_forms = []
            for i in range(0, self.change_form_count):
                kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
                if self.data:
                    kwargs['data'] = self.data
                if self.files:
                    kwargs['files'] = self.files
                if self.initial:
                    kwargs['initial'] = self.initial[i]
                change_form = FormClass(**kwargs)
                self.add_fields(change_form, i)
                change_forms.append(change_form)
            self._change_forms= change_forms
        return self._change_forms
    change_forms = property(_get_change_forms)

    def _forms(self):
        return self.change_forms + self.add_forms
    forms = property(_forms)

    def non_form_errors(self):
        """
        Returns an ErrorList of errors that aren't associated with a particular
        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
        are none.
        """
        if hasattr(self, '_non_form_errors'):
            return self._non_form_errors
        return self.error_class()

    def full_clean(self):
        """Cleans all of self.data and populates self.__errors and self.cleaned_data."""
        self._is_valid = True # Assume the formset is valid until proven otherwise.
        errors = []
        if not self.is_bound: # Stop further processing.
            self.__errors = errors
            return
        self.cleaned_data = []
        self.deleted_data = []
        # Process change forms
        for form in self.change_forms:
            if form.is_valid():
                if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
                    self.deleted_data.append(form.cleaned_data)
                else:
                    self.cleaned_data.append(form.cleaned_data)
            else:
                self._is_valid = False
            errors.append(form.errors)
        # Process add forms in reverse so we can easily tell when the remaining
        # ones should be required.
        reamining_forms_required = False
        add_errors = []
        for i in range(len(self.add_forms)-1, -1, -1):
            form = self.add_forms[i]
            # If an add form is empty, reset it so it won't have any errors
            if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required:
                form.reset()
                continue
            else:
                reamining_forms_required = True
                if form.is_valid():
                    self.cleaned_data.append(form.cleaned_data)
                else:
                    self._is_valid = False
            add_errors.append(form.errors)
        add_errors.reverse()
        errors.extend(add_errors)
        # Sort cleaned_data if the formset is orderable.
        if self.orderable:
            self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
        # Give self.clean() a chance to do validation
        try:
            self.cleaned_data = self.clean()
        except ValidationError, e:
            self._non_form_errors = e.messages
            self._is_valid = False
        self.errors = errors
        # If there were errors, be consistent with forms and remove the
        # cleaned_data and deleted_data attributes.
        if not self._is_valid:
            delattr(self, 'cleaned_data')
            delattr(self, 'deleted_data')

    def clean(self):
        """
        Hook for doing any extra formset-wide cleaning after Form.clean() has
        been called on every form. Any ValidationError raised by this method
        will not be associated with a particular form; it will be accesible
        via formset.non_form_errors()
        """
        return self.cleaned_data

    def add_fields(self, form, index):
        """A hook for adding extra fields on to each form instance."""
        if self.orderable:
            form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
        if self.deletable:
            form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)

    def add_prefix(self, index):
        return '%s-%s' % (self.prefix, index)

    def is_valid(self):
        if not self.is_bound:
            return False
        self.full_clean()
        return self._is_valid

    def _get_media(self):
        # All the forms on a FormSet are the same, so you only need to 
        # interrogate the first form for media.
        if self.forms:
            return self.forms[0].media
        else:
            return Media()
    media = property(_get_media)
    
def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
    """Return a FormSet for the given form class."""
    attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
    return type(form.__name__ + 'FormSet', (formset,), attrs)

def all_valid(formsets):
    """Returns true if every formset in formsets is valid."""
    valid = True
    for formset in formsets:
        if not formset.is_valid():
            valid = False
    return valid