summaryrefslogtreecommitdiff
path: root/django/newforms/formsets.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/newforms/formsets.py')
-rw-r--r--django/newforms/formsets.py213
1 files changed, 104 insertions, 109 deletions
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py
index c2649640a0..330fb088b0 100644
--- a/django/newforms/formsets.py
+++ b/django/newforms/formsets.py
@@ -5,6 +5,11 @@ FORM_COUNT_FIELD_NAME = 'COUNT'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
+def formset_for_form(form, 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', (BaseFormSet,), attrs)
+
class ManagementForm(forms.Form):
"""
``ManagementForm`` is used to keep track of how many form instances
@@ -15,140 +20,130 @@ class ManagementForm(forms.Form):
self.base_fields[FORM_COUNT_FIELD_NAME] = forms.IntegerField(widget=forms.HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs)
-class FormSet(object):
+class BaseFormSet(object):
"""A collection of instances of the same Form class."""
- def __init__(self, form_class, data=None, auto_id='id_%s', prefix=None, initial=None):
- self.form_class = form_class
+ def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
+ self.is_bound = data is not None
self.prefix = prefix or 'form'
self.auto_id = auto_id
+ self.data = data
+ self.initial = initial
# initialization is different depending on whether we recieved data, initial, or nothing
if data:
self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
if self.management_form.is_valid():
- form_count = self.management_form.clean_data[FORM_COUNT_FIELD_NAME]
+ self.total_forms = self.management_form.clean_data[FORM_COUNT_FIELD_NAME]
+ self.required_forms = self.total_forms - self.num_extra
else:
# not sure that ValidationError is the best thing to raise here
raise forms.ValidationError('ManagementForm data is missing or has been tampered with')
- self.form_list = self._forms_for_data(data, form_count=form_count)
elif initial:
- form_count = len(initial)
- self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: form_count+1}, auto_id=self.auto_id, prefix=self.prefix)
- self.form_list = self._forms_for_initial(initial, form_count=form_count)
+ 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.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: 1}, auto_id=self.auto_id, prefix=self.prefix)
- self.form_list = self._empty_forms(form_count=1)
-
- # TODO: initialization needs some cleanup and some restructuring
- # TODO: allow more than 1 extra blank form to be displayed
+ 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 _forms_for_data(self, data, form_count):
- form_list = []
- for i in range(0, form_count-1):
- form_instance = self.form_class(data, auto_id=self.auto_id, prefix=self.add_prefix(i))
- self.add_fields(form_instance, i)
- form_list.append(form_instance)
- # hackish, but if the last form stayed empty, replace it with a
- # blank one. no 'data' or 'initial' arguments
- form_instance = self.form_class(data, auto_id=self.auto_id, prefix=self.add_prefix(form_count-1))
- if form_instance.is_empty():
- form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(form_count-1))
- self.add_fields(form_instance, form_count-1)
- form_list.append(form_instance)
- return form_list
+ def _get_form_list(self):
+ """Return a list of Form instances."""
+ if not hasattr(self, '_form_list'):
+ self._form_list = []
+ for i in range(0, self.total_forms):
+ kwargs = {'data': self.data, 'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+ if self.initial and i < self.required_forms:
+ kwargs['initial'] = self.initial[i]
+ form_instance = self.form_class(**kwargs)
+ # HACK: if the form was not completed, replace it with a blank one
+ if self.data and i >= self.required_forms and form_instance.is_empty():
+ form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i))
+ self.add_fields(form_instance, i)
+ self._form_list.append(form_instance)
+ return self._form_list
- def _forms_for_initial(self, initial, form_count):
- form_list = []
- # generate a form for each item in initial, plus one empty one
- for i in range(0, form_count):
- form_instance = self.form_class(initial=initial[i], auto_id=self.auto_id, prefix=self.add_prefix(i))
- self.add_fields(form_instance, i)
- form_list.append(form_instance)
- # add 1 empty form
- form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i+1))
- self.add_fields(form_instance, i+1)
- form_list.append(form_instance)
- return form_list
+ form_list = property(_get_form_list)
- def _empty_forms(self, form_count):
- form_list = []
- # we only need one form, there's no inital data and no post data
- form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(0))
- form_list.append(form_instance)
- return form_list
+ def full_clean(self):
+ """
+ Cleans all of self.data and populates self.__errors and self.clean_data.
+ """
+ is_valid = True
+
+ errors = []
+ if not self.is_bound: # Stop further processing.
+ self.__errors = errors
+ return
+ clean_data = []
+ deleted_data = []
+
+ self._form_list = []
+ # step backwards through the forms so when we hit the first filled one
+ # we can easily require the rest without backtracking
+ required = False
+ for i in range(self.total_forms-1, -1, -1):
+ kwargs = {'data': self.data, 'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+
+ # prep initial data if there is some
+ if self.initial and i < self.required_forms:
+ kwargs['initial'] = self.initial[i]
+
+ # create the form instance
+ form = self.form_class(**kwargs)
+ self.add_fields(form, i)
+
+ if self.data and (i < self.required_forms or not form.is_empty(exceptions=[ORDERING_FIELD_NAME])):
+ required = True # forms cannot be empty anymore
+
+ # HACK: if the form is empty and not required, replace it with a blank one
+ # this is necessary to keep form.errors empty
+ if not required and self.data and form.is_empty(exceptions=[ORDERING_FIELD_NAME]):
+ form = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i))
+ self.add_fields(form, i)
+ else:
+ # if the formset is still vaild overall and this form instance
+ # is valid, keep appending to clean_data
+ if is_valid and form.is_valid():
+ if self.deletable and form.clean_data[DELETION_FIELD_NAME]:
+ deleted_data.append(form.clean_data)
+ else:
+ clean_data.append(form.clean_data)
+ else:
+ is_valid = False
+ # append to errors regardless
+ errors.append(form.errors)
+ self._form_list.append(form)
- def get_forms(self):
- return self.form_list
+ deleted_data.reverse()
+ if self.orderable:
+ clean_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
+ else:
+ clean_data.reverse()
+ errors.reverse()
+ self._form_list.reverse()
+
+ if is_valid:
+ self.clean_data = clean_data
+ self.deleted_data = deleted_data
+ self.errors = errors
+ self._is_valid = is_valid
+ # TODO: user defined formset validation.
+
def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance."""
- pass
+ if self.orderable:
+ form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1)
+ if self.deletable:
+ form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False)
def add_prefix(self, index):
return '%s-%s' % (self.prefix, index)
- def _get_clean_data(self):
- return self.get_clean_data()
-
- def get_clean_data(self):
- clean_data_list = []
- for form in self.get_non_empty_forms():
- clean_data_list.append(form.clean_data)
- return clean_data_list
-
- clean_data = property(_get_clean_data)
-
- def is_valid(self):
- for form in self.get_non_empty_forms():
- if not form.is_valid():
- return False
- return True
-
- def get_non_empty_forms(self):
- """Return all forms that aren't empty."""
- return [form for form in self.form_list if not form.is_empty()]
-
-class FormSetWithDeletion(FormSet):
- """A ``FormSet`` that handles deletion of forms."""
-
- def add_fields(self, form, index):
- """Add a delete checkbox to each form."""
- form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False)
-
- def get_clean_data(self):
- self.deleted_data = []
- clean_data_list = []
- for form in self.get_non_empty_forms():
- if form.clean_data[DELETION_FIELD_NAME]:
- # stick data marked for deletetion in self.deleted_data
- self.deleted_data.append(form.clean_data)
- else:
- clean_data_list.append(form.clean_data)
- return clean_data_list
-
-class FormSetWithOrdering(FormSet):
- """A ``FormSet`` that handles re-ordering of forms."""
-
- def get_non_empty_forms(self):
- return [form for form in self.form_list if not form.is_empty(exceptions=[ORDERING_FIELD_NAME])]
-
- def add_fields(self, form, index):
- """Add an ordering field to each form."""
- form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1)
-
- def get_clean_data(self):
- clean_data_list = []
- for form in self.get_non_empty_forms():
- clean_data_list.append(form.clean_data)
- # sort clean_data by the 'ORDER' field
- clean_data_list.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
- return clean_data_list
-
def is_valid(self):
- for form in self.get_non_empty_forms():
- if not form.is_valid():
- return False
- return True
+ self.full_clean()
+ return self._is_valid
# TODO: handle deletion and ordering in the same FormSet
# TODO: model integration: form_for_instance and form_for_model type functions