Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

newforms-admin: Fixed #5353. Added FormSet validation hook. Separated…

… a few things out from the original patch and added more tests. Thanks, Honza Kral.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@6419 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit b40f9b63bb6900689e6052052f620d6447ec63e5 1 parent 9687447
Joseph Kocherhans jkocherhans authored
Showing with 111 additions and 24 deletions.
  1. +48 −22 django/newforms/formsets.py
  2. +63 −2 tests/regressiontests/forms/formsets.py
70 django/newforms/formsets.py
View
@@ -1,6 +1,7 @@
-from forms import Form, ValidationError
+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')
@@ -22,13 +23,15 @@ def __init__(self, *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):
+ 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)
@@ -92,55 +95,78 @@ 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."""
- is_valid = True
+ 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
- cleaned_data = []
- deleted_data = []
-
+ 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]:
- deleted_data.append(form.cleaned_data)
+ self.deleted_data.append(form.cleaned_data)
else:
- cleaned_data.append(form.cleaned_data)
+ self.cleaned_data.append(form.cleaned_data)
else:
- is_valid = False
+ 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.
- required = False
+ 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 required:
+ if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required:
form.reset()
continue
else:
- required = True
+ reamining_forms_required = True
if form.is_valid():
- cleaned_data.append(form.cleaned_data)
+ self.cleaned_data.append(form.cleaned_data)
else:
- is_valid = False
+ 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:
- 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.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
- self._is_valid = is_valid
+ # 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."""
65 tests/regressiontests/forms/formsets.py
View
@@ -5,8 +5,8 @@
FormSet allows us to use multiple instance of the same form on 1 page. For now,
the best way to create a FormSet is by using the formset_for_form function.
->>> from django.newforms import Form, CharField, IntegerField
->>> from django.newforms.formsets import formset_for_form
+>>> from django.newforms import Form, CharField, IntegerField, ValidationError
+>>> from django.newforms.formsets import formset_for_form, BaseFormSet
>>> class Choice(Form):
... choice = CharField()
@@ -420,4 +420,65 @@
[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]
+# FormSet clean hook ##########################################################
+
+FormSets have a hook for doing extra validation that shouldn't be tied to any
+particular form. It follows the same pattern as the clean hook on Forms.
+
+Let's define a FormSet that takes a list of favorite drinks, but raises am
+error if there are any duplicates.
+
+>>> class FavoriteDrinkForm(Form):
+... name = CharField()
+...
+
+>>> class FavoriteDrinksFormSet(BaseFormSet):
+... form_class = FavoriteDrinkForm
+... num_extra = 2
+... orderable = False
+... deletable = False
+...
+... def clean(self):
+... seen_drinks = []
+... for drink in self.cleaned_data:
+... if drink['name'] in seen_drinks:
+... raise ValidationError('You may only specify a drink once.')
+... seen_drinks.append(drink['name'])
+... return self.cleaned_data
+...
+
+We start out with a some duplicate data.
+
+>>> data = {
+... 'drinks-COUNT': '2',
+... 'drinks-0-name': 'Gin and Tonic',
+... 'drinks-1-name': 'Gin and Tonic',
+... }
+
+>>> formset = FavoriteDrinksFormSet(data, prefix='drinks')
+>>> formset.is_valid()
+False
+
+Any errors raised by formset.clean() are available via the
+formset.non_form_errors() method.
+
+>>> for error in formset.non_form_errors():
+... print error
+You may only specify a drink once.
+
+
+Make sure we didn't break the valid case.
+
+>>> data = {
+... 'drinks-COUNT': '2',
+... 'drinks-0-name': 'Gin and Tonic',
+... 'drinks-1-name': 'Bloody Mary',
+... }
+
+>>> formset = FavoriteDrinksFormSet(data, prefix='drinks')
+>>> formset.is_valid()
+True
+>>> for error in formset.non_form_errors():
+... print error
+
"""
Please sign in to comment.
Something went wrong with that request. Please try again.