summaryrefslogtreecommitdiff
path: root/django/newforms/formsets.py
blob: 38982b47de14aa1d0c9161c35f29f32e3c7a70f6 (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
from forms import Form, ValidationError
from fields import IntegerField, BooleanField
from widgets import HiddenInput, Media

# 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):
        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
        # 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, '_add_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 full_clean(self):
        """Cleans all of self.data and populates self.__errors and self.cleaned_data."""
        is_valid = True
        errors = []
        if not self.is_bound: # Stop further processing.
            self.__errors = errors
            return
        cleaned_data = []
        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]:
                    deleted_data.append(form.cleaned_data)
                else:
                    cleaned_data.append(form.cleaned_data)
            else:
                is_valid = False
            errors.append(form.errors)
        
        # Process add forms in reverse so we can easily tell when the remaining
        # ones should be required.
        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 required:
                form.reset()
                continue
            else:
                required = True
                if form.is_valid():
                    cleaned_data.append(form.cleaned_data)
                else:
                    is_valid = False
            add_errors.append(form.errors)
        add_errors.reverse()
        errors.extend(add_errors)

        if self.orderable:
            cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])

        if is_valid:
            self.cleaned_data = cleaned_data
            self.deleted_data = deleted_data
        self.errors = errors
        self._is_valid = is_valid

    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):
        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."""
    for formset in formsets:
        if not formset.is_valid():
            return False
    return True