1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

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
This commit is contained in:
Joseph Kocherhans 2007-09-25 02:42:26 +00:00
parent 9687447e31
commit b40f9b63bb
2 changed files with 111 additions and 24 deletions

View File

@ -1,6 +1,7 @@
from forms import Form, ValidationError from forms import Form
from fields import IntegerField, BooleanField from fields import IntegerField, BooleanField
from widgets import HiddenInput, Media from widgets import HiddenInput, Media
from util import ErrorList, ValidationError
__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid') __all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
@ -22,13 +23,15 @@ class ManagementForm(Form):
class BaseFormSet(object): class BaseFormSet(object):
"""A collection of instances of the same Form class.""" """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.is_bound = data is not None or files is not None
self.prefix = prefix or 'form' self.prefix = prefix or 'form'
self.auto_id = auto_id self.auto_id = auto_id
self.data = data self.data = data
self.files = files self.files = files
self.initial = initial self.initial = initial
self.error_class = error_class
# initialization is different depending on whether we recieved data, initial, or nothing # initialization is different depending on whether we recieved data, initial, or nothing
if data or files: if data or files:
self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
@ -92,55 +95,78 @@ class BaseFormSet(object):
return self.change_forms + self.add_forms return self.change_forms + self.add_forms
forms = property(_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): def full_clean(self):
"""Cleans all of self.data and populates self.__errors and self.cleaned_data.""" """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 = [] errors = []
if not self.is_bound: # Stop further processing. if not self.is_bound: # Stop further processing.
self.__errors = errors self.__errors = errors
return return
cleaned_data = [] self.cleaned_data = []
deleted_data = [] self.deleted_data = []
# Process change forms # Process change forms
for form in self.change_forms: for form in self.change_forms:
if form.is_valid(): if form.is_valid():
if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
deleted_data.append(form.cleaned_data) self.deleted_data.append(form.cleaned_data)
else: else:
cleaned_data.append(form.cleaned_data) self.cleaned_data.append(form.cleaned_data)
else: else:
is_valid = False self._is_valid = False
errors.append(form.errors) errors.append(form.errors)
# Process add forms in reverse so we can easily tell when the remaining # Process add forms in reverse so we can easily tell when the remaining
# ones should be required. # ones should be required.
required = False reamining_forms_required = False
add_errors = [] add_errors = []
for i in range(len(self.add_forms)-1, -1, -1): for i in range(len(self.add_forms)-1, -1, -1):
form = self.add_forms[i] form = self.add_forms[i]
# If an add form is empty, reset it so it won't have any errors # 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() form.reset()
continue continue
else: else:
required = True reamining_forms_required = True
if form.is_valid(): if form.is_valid():
cleaned_data.append(form.cleaned_data) self.cleaned_data.append(form.cleaned_data)
else: else:
is_valid = False self._is_valid = False
add_errors.append(form.errors) add_errors.append(form.errors)
add_errors.reverse() add_errors.reverse()
errors.extend(add_errors) errors.extend(add_errors)
# Sort cleaned_data if the formset is orderable.
if self.orderable: if self.orderable:
cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
# Give self.clean() a chance to do validation
if is_valid: try:
self.cleaned_data = cleaned_data self.cleaned_data = self.clean()
self.deleted_data = deleted_data except ValidationError, e:
self._non_form_errors = e.messages
self._is_valid = False
self.errors = errors 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): def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance.""" """A hook for adding extra fields on to each form instance."""

View File

@ -5,8 +5,8 @@ formset_tests = """
FormSet allows us to use multiple instance of the same form on 1 page. For now, 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. 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 import Form, CharField, IntegerField, ValidationError
>>> from django.newforms.formsets import formset_for_form >>> from django.newforms.formsets import formset_for_form, BaseFormSet
>>> class Choice(Form): >>> class Choice(Form):
... choice = CharField() ... choice = CharField()
@ -420,4 +420,65 @@ True
[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}] [{'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
""" """