diff options
| author | Joseph Kocherhans <joseph@jkocherhans.com> | 2007-04-07 05:56:42 +0000 |
|---|---|---|
| committer | Joseph Kocherhans <joseph@jkocherhans.com> | 2007-04-07 05:56:42 +0000 |
| commit | 203f93a7aa9fbe04fa660c262ed2e730de437faf (patch) | |
| tree | 8b2e49d5a160a8a325917eae45c0e8ad90c89777 /django/newforms/formsets.py | |
| parent | c1f84583de0993cba8f6a66008b59744e0ddc967 (diff) | |
newforms-admin: Rewrote most of FormSets and associated tests. They now handle an arbitrary number of extra forms, and deletion and ordering at the same time. BaseFormSet still needs some cleanup, but the tests pass.
git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@4953 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/newforms/formsets.py')
| -rw-r--r-- | django/newforms/formsets.py | 213 |
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 |
