diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index ec32c897a5..7f958018b0 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1,6 +1,7 @@
from django import oldforms, template
from django import newforms as forms
from django.newforms.formsets import all_valid
+from django.newforms.models import _modelform_factory, _inlineformset_factory
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets
from django.contrib.admin.util import get_deleted_objects
@@ -340,7 +341,7 @@ class ModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
+ return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
def form_change(self, request, obj):
"""
@@ -350,7 +351,7 @@ class ModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)
+ return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
def save_add(self, request, model, form, formsets, post_url_continue):
"""
@@ -496,14 +497,14 @@ class ModelAdmin(BaseModelAdmin):
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
for FormSet in self.formsets_add(request):
- inline_formset = FormSet(obj, data=request.POST, files=request.FILES)
+ inline_formset = FormSet(data=request.POST, files=request.FILES, instance=obj)
inline_formsets.append(inline_formset)
if all_valid(inline_formsets) and form.is_valid():
return self.save_add(request, model, form, inline_formsets, '../%s/')
else:
form = ModelForm(initial=request.GET)
for FormSet in self.formsets_add(request):
- inline_formset = FormSet(obj)
+ inline_formset = FormSet(instance=obj)
inline_formsets.append(inline_formset)
adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
@@ -553,17 +554,17 @@ class ModelAdmin(BaseModelAdmin):
ModelForm = self.form_change(request, obj)
inline_formsets = []
if request.method == 'POST':
- form = ModelForm(request.POST, request.FILES)
+ form = ModelForm(request.POST, request.FILES, instance=obj)
for FormSet in self.formsets_change(request, obj):
- inline_formset = FormSet(obj, request.POST, request.FILES)
+ inline_formset = FormSet(request.POST, request.FILES, instance=obj)
inline_formsets.append(inline_formset)
if all_valid(inline_formsets) and form.is_valid():
return self.save_change(request, model, form, inline_formsets)
else:
- form = ModelForm()
+ form = ModelForm(instance=obj)
for FormSet in self.formsets_change(request, obj):
- inline_formset = FormSet(obj)
+ inline_formset = FormSet(instance=obj)
inline_formsets.append(inline_formset)
## Populate the FormWrapper.
@@ -740,7 +741,7 @@ class InlineModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
+ return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
def formset_change(self, request, obj):
"""Returns an InlineFormSet class for use in admin change views."""
@@ -748,18 +749,18 @@ class InlineModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
+ return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
def fieldsets_add(self, request):
if self.declared_fieldsets:
return self.declared_fieldsets
- form = self.formset_add(request).form_class
+ form = self.formset_add(request).form
return [(None, {'fields': form.base_fields.keys()})]
def fieldsets_change(self, request, obj):
if self.declared_fieldsets:
return self.declared_fieldsets
- form = self.formset_change(request, obj).form_class
+ form = self.formset_change(request, obj).form
return [(None, {'fields': form.base_fields.keys()})]
class StackedInline(InlineModelAdmin):
@@ -778,14 +779,14 @@ class InlineAdminFormSet(object):
self.fieldsets = fieldsets
def __iter__(self):
- for form, original in zip(self.formset.change_forms, self.formset.get_queryset()):
+ for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
- for form in self.formset.add_forms:
+ for form in self.formset.extra_forms:
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
def fields(self):
for field_name in flatten_fieldsets(self.fieldsets):
- yield self.formset.form_class.base_fields[field_name]
+ yield self.formset.form.base_fields[field_name]
class InlineAdminForm(AdminForm):
"""
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index 8d8beb0f88..f2a4214268 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -69,7 +69,8 @@ class BaseForm(StrAndUnicode):
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
- initial=None, error_class=ErrorList, label_suffix=':'):
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@@ -78,6 +79,7 @@ class BaseForm(StrAndUnicode):
self.initial = initial or {}
self.error_class = error_class
self.label_suffix = label_suffix
+ self.empty_permitted = empty_permitted
self._errors = None # Stores the errors after clean() has been called.
# The base_fields class attribute is the *class-wide* definition of
@@ -189,24 +191,6 @@ class BaseForm(StrAndUnicode):
"""
return self.errors.get(NON_FIELD_ERRORS, self.error_class())
- def is_empty(self, exceptions=None):
- """
- Returns True if this form has been bound and all fields that aren't
- listed in exceptions are empty.
- """
- # TODO: This could probably use some optimization
- exceptions = exceptions or []
- for name, field in self.fields.items():
- if name in exceptions:
- continue
- # value_from_datadict() gets the data from the data dictionaries.
- # Each widget type knows how to retrieve its own data, because some
- # widgets split data over several HTML fields.
- value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
- if not field.widget.is_empty(value):
- return False
- return True
-
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
@@ -216,6 +200,10 @@ class BaseForm(StrAndUnicode):
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
+ # If the form is permitted to be empty, and none of the form data has
+ # changed from the initial data, short circuit any validation.
+ if self.empty_permitted and not self.has_changed():
+ return
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
@@ -251,11 +239,24 @@ class BaseForm(StrAndUnicode):
"""
return self.cleaned_data
- def reset(self):
- """Return this form to the state it was in before data was passed to it."""
- self.data = {}
- self.is_bound = False
- self.__errors = None
+ def has_changed(self):
+ """
+ Returns True if data differs from initial.
+ """
+ # XXX: For now we're asking the individual widgets whether or not the
+ # data has changed. It would probably be more efficient to hash the
+ # initial data, store it in a hidden field, and compare a hash of the
+ # submitted data, but we'd need a way to easily get the string value
+ # for a given field. Right now, that logic is embedded in the render
+ # method of each widget.
+ for name, field in self.fields.items():
+ prefixed_name = self.add_prefix(name)
+ data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
+ initial_value = self.initial.get(name, field.initial)
+ if field.widget._has_changed(initial_value, data_value):
+ #print field
+ return True
+ return False
def _get_media(self):
"""
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py
index 56179a9b39..a11aed8221 100644
--- a/django/newforms/formsets.py
+++ b/django/newforms/formsets.py
@@ -1,12 +1,14 @@
from forms import Form
+from django.utils.encoding import StrAndUnicode
from fields import IntegerField, BooleanField
-from widgets import HiddenInput, Media
+from widgets import HiddenInput, TextInput
from util import ErrorList, ValidationError
-__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
+__all__ = ('BaseFormSet', 'all_valid')
# special field names
-FORM_COUNT_FIELD_NAME = 'COUNT'
+TOTAL_FORM_COUNT = 'TOTAL_FORMS'
+INITIAL_FORM_COUNT = 'INITIAL_FORMS'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
@@ -17,13 +19,15 @@ class ManagementForm(Form):
increment the count field of this form as well.
"""
def __init__(self, *args, **kwargs):
- self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
+ self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[INITIAL_FORM_COUNT] = 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,
+class BaseFormSet(StrAndUnicode):
+ """
+ A collection of instances of the same Form class.
+ """
+ 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'
@@ -32,68 +36,132 @@ class BaseFormSet(object):
self.files = files
self.initial = initial
self.error_class = error_class
+ self._errors = None
+ self._non_form_errors = None
# 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
+ self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
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)
+ self._initial_form_count = len(initial)
+ self._total_form_count = self._initial_form_count + self.extra
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)
+ self._initial_form_count = 0
+ self._total_form_count = self.extra
+ initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count}
+ self.management_form = ManagementForm(initial=initial, auto_id=auto_id, prefix=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)
+ # instantiate all the forms and put them in self.forms
+ self.forms = []
+ for i in range(self._total_form_count):
+ self.forms.append(self._construct_form(i))
- def _get_change_forms(self):
- """Return a list of all the change forms in this ``FormSet``."""
- FormClass = self.form_class
- if not hasattr(self, '_change_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 __unicode__(self):
+ return self.as_table()
- def _forms(self):
- return self.change_forms + self.add_forms
- forms = property(_forms)
+ def _construct_form(self, i):
+ """
+ Instantiates and returns the i-th form instance in a formset.
+ """
+ kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+ if self.data or self.files:
+ kwargs['data'] = self.data
+ kwargs['files'] = self.files
+ if self.initial:
+ try:
+ kwargs['initial'] = self.initial[i]
+ except IndexError:
+ pass
+ # Allow extra forms to be empty.
+ if i >= self._initial_form_count:
+ kwargs['empty_permitted'] = True
+ form = self.form(**kwargs)
+ self.add_fields(form, i)
+ return form
+
+ def _get_initial_forms(self):
+ """Return a list of all the intial forms in this formset."""
+ return self.forms[:self._initial_form_count]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ """Return a list of all the extra forms in this formset."""
+ return self.forms[self._initial_form_count:]
+ extra_forms = property(_get_extra_forms)
+
+ # Maybe this should just go away?
+ def _get_cleaned_data(self):
+ """
+ Returns a list of form.cleaned_data dicts for every form in self.forms.
+ """
+ if not self.is_valid():
+ raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
+ return [form.cleaned_data for form in self.forms]
+ cleaned_data = property(_get_cleaned_data)
+
+ def _get_deleted_forms(self):
+ """
+ Returns a list of forms that have been marked for deletion. Raises an
+ AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_delete:
+ raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
+ # construct _deleted_form_indexes which is just a list of form indexes
+ # that have had their deletion widget set to True
+ if not hasattr(self, '_deleted_form_indexes'):
+ self._deleted_form_indexes = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ if form.cleaned_data[DELETION_FIELD_NAME]:
+ self._deleted_form_indexes.append(i)
+ return [self.forms[i] for i in self._deleted_form_indexes]
+ deleted_forms = property(_get_deleted_forms)
+
+ def _get_ordered_forms(self):
+ """
+ Returns a list of form in the order specified by the incoming data.
+ Raises an AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_order:
+ raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
+ # Construct _ordering, which is a list of (form_index, order_field_value)
+ # tuples. After constructing this list, we'll sort it by order_field_value
+ # so we have a way to get to the form indexes in the order specified
+ # by the form data.
+ if not hasattr(self, '_ordering'):
+ self._ordering = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ # don't add data marked for deletion to self.ordered_data
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ # A sort function to order things numerically ascending, but
+ # None should be sorted below anything else. Allowing None as
+ # a comparison value makes it so we can leave ordering fields
+ # blamk.
+ def compare_ordering_values(x, y):
+ if x[1] is None:
+ return 1
+ if y[1] is None:
+ return -1
+ return x[1] - y[1]
+ self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
+ # After we're done populating self._ordering, sort it.
+ self._ordering.sort(compare_ordering_values)
+ # Return a list of form.cleaned_data dicts in the order spcified by
+ # the form data.
+ return [self.forms[i[0]] for i in self._ordering]
+ ordered_forms = property(_get_ordered_forms)
def non_form_errors(self):
"""
@@ -101,63 +169,48 @@ class BaseFormSet(object):
form -- i.e., from formset.clean(). Returns an empty ErrorList if there
are none.
"""
- if hasattr(self, '_non_form_errors'):
+ if self._non_form_errors is not None:
return self._non_form_errors
return self.error_class()
+ def _get_errors(self):
+ """
+ Returns a list of form.errors for every form in self.forms.
+ """
+ if self._errors is None:
+ self.full_clean()
+ return self._errors
+ errors = property(_get_errors)
+
+ def is_valid(self):
+ """
+ Returns True if form.errors is empty for every form in self.forms.
+ """
+ if not self.is_bound:
+ return False
+ # We loop over every form.errors here rather than short circuiting on the
+ # first failure to make sure validation gets triggered for every form.
+ forms_valid = True
+ for errors in self.errors:
+ if bool(errors):
+ forms_valid = False
+ return forms_valid and not bool(self.non_form_errors())
+
def full_clean(self):
- """Cleans all of self.data and populates self.__errors and self.cleaned_data."""
- self._is_valid = True # Assume the formset is valid until proven otherwise.
- errors = []
+ """
+ Cleans all of self.data and populates self._errors.
+ """
+ self._errors = []
if not self.is_bound: # Stop further processing.
- self.__errors = errors
return
- 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]:
- self.deleted_data.append(form.cleaned_data)
- else:
- self.cleaned_data.append(form.cleaned_data)
- else:
- 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.
- 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 reamining_forms_required:
- form.reset()
- continue
- else:
- reamining_forms_required = True
- if form.is_valid():
- self.cleaned_data.append(form.cleaned_data)
- else:
- 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:
- self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
- # Give self.clean() a chance to do validation
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ self._errors.append(form.errors)
+ # Give self.clean() a chance to do cross-form validation.
try:
- self.cleaned_data = self.clean()
+ self.clean()
except ValidationError, e:
self._non_form_errors = e.messages
- self._is_valid = False
- self.errors = errors
- # 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):
"""
@@ -166,36 +219,50 @@ class BaseFormSet(object):
will not be associated with a particular form; it will be accesible
via formset.non_form_errors()
"""
- return self.cleaned_data
+ pass
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:
+ if self.can_order:
+ # Only pre-fill the ordering field for initial forms.
+ if index < self._initial_form_count:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
+ else:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
+ if self.can_delete:
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):
- if not self.is_bound:
- return False
- self.full_clean()
- return self._is_valid
+ def is_multipart(self):
+ """
+ Returns True if the formset needs to be multipart-encrypted, i.e. it
+ has FileInput. Otherwise, False.
+ """
+ return self.forms[0].is_multipart()
def _get_media(self):
- # All the forms on a FormSet are the same, so you only need to
+ # 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):
+
+ def as_table(self):
+ "Returns this formset rendered as HTML
s -- excluding the
."
+ # XXX: there is no semantic division between forms here, there
+ # probably should be. It might make sense to render each form as a
+ # table row with each field as a td.
+ forms = u' '.join([form.as_table() for form in self.forms])
+ return u'\n'.join([unicode(self.management_form), forms])
+
+# XXX: This API *will* change. Use at your own risk.
+def _formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False):
"""Return a FormSet for the given form class."""
- attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
+ attrs = {'form': form, 'extra': extra, 'can_order': can_order, 'can_delete': can_delete}
return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets):
diff --git a/django/newforms/models.py b/django/newforms/models.py
index 3d71ee64e5..81d59fa22e 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -13,13 +13,12 @@ from django.core.exceptions import ImproperlyConfigured
from util import ValidationError, ErrorList
from forms import BaseForm, get_declared_fields
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
-from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
+from formsets import BaseFormSet, _formset_factory, DELETION_FIELD_NAME
__all__ = (
'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
- 'formset_for_model', 'inline_formset',
'ModelChoiceField', 'ModelMultipleChoiceField',
)
@@ -245,7 +244,7 @@ class ModelFormMetaclass(type):
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':',
- instance=None):
+ empty_permitted=False, instance=None):
opts = self._meta
if instance is None:
# if we didn't get an instance, instantiate a new one
@@ -257,7 +256,8 @@ class BaseModelForm(BaseForm):
# if initial was provided, it should override the values from instance
if initial is not None:
object_data.update(initial)
- BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix)
+ BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
+ error_class, label_suffix, empty_permitted)
def save(self, commit=True):
"""
@@ -276,6 +276,182 @@ class BaseModelForm(BaseForm):
class ModelForm(BaseModelForm):
__metaclass__ = ModelFormMetaclass
+# XXX: This API *will* change. Use at your own risk.
+def _modelform_factory(model, form=BaseForm, fields=None, exclude=None,
+ formfield_callback=lambda f: f.formfield()):
+ # HACK: we should be able to construct a ModelForm without creating
+ # and passing in a temporary inner class
+ class Meta:
+ pass
+ setattr(Meta, 'model', model)
+ setattr(Meta, 'fields', fields)
+ setattr(Meta, 'exclude', exclude)
+ class_name = model.__name__ + 'Form'
+ return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta},
+ formfield_callback=formfield_callback)
+
+
+# ModelFormSets ##############################################################
+
+class BaseModelFormSet(BaseFormSet):
+ """
+ A ``FormSet`` for editing a queryset and/or adding new objects to it.
+ """
+ model = None
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None):
+ self.queryset = queryset
+ kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
+ kwargs['initial'] = [model_to_dict(obj) for obj in self.get_queryset()]
+ super(BaseModelFormSet, self).__init__(**kwargs)
+
+ def get_queryset(self):
+ if self.queryset is not None:
+ return self.queryset
+ return self.model._default_manager.get_query_set()
+
+ def save_new(self, form, commit=True):
+ """Saves and returns a new model instance for the given form."""
+ return save_instance(form, self.model(), commit=commit)
+
+ def save_existing(self, form, instance, commit=True):
+ """Saves and returns an existing model instance for the given form."""
+ return save_instance(form, instance, commit=commit)
+
+ def save(self, commit=True):
+ """Saves model instances for every form, adding and changing instances
+ as necessary, and returns the list of instances.
+ """
+ return self.save_existing_objects(commit) + self.save_new_objects(commit)
+
+ def save_existing_objects(self, commit=True):
+ if not self.get_queryset():
+ return []
+ # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
+ existing_objects = {}
+ for obj in self.get_queryset():
+ existing_objects[obj.pk] = obj
+ saved_instances = []
+ for form in self.initial_forms:
+ obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ obj.delete()
+ else:
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ return saved_instances
+
+ def save_new_objects(self, commit=True):
+ new_objects = []
+ for form in self.extra_forms:
+ if not form.has_changed():
+ continue
+ # If someone has marked an add form for deletion, don't save the
+ # object. At some point it would be nice if we didn't display
+ # the deletion widget for add forms.
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ new_objects.append(self.save_new(form, commit=commit))
+ return new_objects
+
+ def add_fields(self, form, index):
+ """Add a hidden field for the object's primary key."""
+ self._pk_field_name = self.model._meta.pk.attname
+ form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
+ super(BaseModelFormSet, self).add_fields(form, index)
+
+# XXX: Use at your own risk. This API *will* change.
+def _modelformset_factory(model, form=BaseModelForm, formfield_callback=lambda f: f.formfield(),
+ formset=BaseModelFormSet,
+ extra=1, can_delete=False, can_order=False,
+ fields=None, exclude=None):
+ """
+ Returns a FormSet class for the given Django model class.
+ """
+ form = _modelform_factory(model, form=form, fields=fields, exclude=exclude,
+ formfield_callback=formfield_callback)
+ FormSet = _formset_factory(form, formset, extra=extra, can_order=can_order, can_delete=can_delete)
+ FormSet.model = model
+ return FormSet
+
+
+# InlineFormSets #############################################################
+
+class BaseInlineFormset(BaseModelFormSet):
+ """A formset for child objects related to a parent."""
+ def __init__(self, data=None, files=None, instance=None):
+ from django.db.models.fields.related import RelatedObject
+ self.instance = instance
+ # is there a better way to get the object descriptor?
+ self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
+ super(BaseInlineFormset, self).__init__(data, files, prefix=self.rel_name)
+
+ def get_queryset(self):
+ """
+ Returns this FormSet's queryset, but restricted to children of
+ self.instance
+ """
+ kwargs = {self.fk.name: self.instance}
+ return self.model._default_manager.filter(**kwargs)
+
+ def save_new(self, form, commit=True):
+ kwargs = {self.fk.get_attname(): self.instance.pk}
+ new_obj = self.model(**kwargs)
+ return save_instance(form, new_obj, commit=commit)
+
+def _get_foreign_key(parent_model, model, fk_name=None):
+ """
+ Finds and returns the ForeignKey from model to parent if there is one.
+ If fk_name is provided, assume it is the name of the ForeignKey field.
+ """
+ # avoid circular import
+ from django.db.models import ForeignKey
+ opts = model._meta
+ if fk_name:
+ fks_to_parent = [f for f in opts.fields if f.name == fk_name]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
+ raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no field named '%s'" % (model, fk_name))
+ else:
+ # Try to discover what the ForeignKey from model to parent_model is
+ fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
+ else:
+ raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
+ return fk
+
+
+# XXX: This API *will* change. Use at your own risk.
+def _inlineformset_factory(parent_model, model, form=BaseModelForm, fk_name=None,
+ fields=None, exclude=None,
+ extra=3, can_order=False, can_delete=True,
+ formfield_callback=lambda f: f.formfield()):
+ """
+ Returns an ``InlineFormset`` for the given kwargs.
+
+ You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
+ to ``parent_model``.
+ """
+ fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
+ # let the formset handle object deletion by default
+
+ if exclude is not None:
+ exclude.append(fk.name)
+ else:
+ exclude = [fk.name]
+ FormSet = _modelformset_factory(model, form=form,
+ formfield_callback=formfield_callback,
+ formset=BaseInlineFormset,
+ extra=extra, can_delete=can_delete, can_order=can_order,
+ fields=fields, exclude=exclude)
+ FormSet.fk = fk
+ return FormSet
+
# Fields #####################################################################
@@ -387,180 +563,3 @@ class ModelMultipleChoiceField(ModelChoiceField):
else:
final_values.append(obj)
return final_values
-
-# Model-FormSet integration ###################################################
-
-def initial_data(instance, fields=None):
- """
- Return a dictionary from data in ``instance`` that is suitable for
- use as a ``Form`` constructor's ``initial`` argument.
-
- Provide ``fields`` to specify the names of specific fields to return.
- All field values in the instance will be returned if ``fields`` is not
- provided.
- """
- # avoid a circular import
- from django.db.models.fields.related import ManyToManyField
- opts = instance._meta
- initial = {}
- for f in opts.fields + opts.many_to_many:
- if not f.editable:
- continue
- if fields and not f.name in fields:
- continue
- if isinstance(f, ManyToManyField):
- # MultipleChoiceWidget needs a list of ints, not object instances.
- initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
- else:
- initial[f.name] = f.value_from_object(instance)
- return initial
-
-class BaseModelFormSet(BaseFormSet):
- """
- A ``FormSet`` for editing a queryset and/or adding new objects to it.
- """
- model = None
- queryset = None
-
- def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):
- kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
- self.queryset = qs
- kwargs['initial'] = [initial_data(obj) for obj in qs]
- super(BaseModelFormSet, self).__init__(**kwargs)
-
- def save_new(self, form, commit=True):
- """Saves and returns a new model instance for the given form."""
- return save_instance(form, self.model(), commit=commit)
-
- def save_instance(self, form, instance, commit=True):
- """Saves and returns an existing model instance for the given form."""
- return save_instance(form, instance, commit=commit)
-
- def save(self, commit=True):
- """Saves model instances for every form, adding and changing instances
- as necessary, and returns the list of instances.
- """
- return self.save_existing_objects(commit) + self.save_new_objects(commit)
-
- def save_existing_objects(self, commit=True):
- if not self.queryset:
- return []
- # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
- existing_objects = {}
- for obj in self.queryset:
- existing_objects[obj.pk] = obj
- saved_instances = []
- for form in self.change_forms:
- obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
- if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
- obj.delete()
- else:
- saved_instances.append(self.save_instance(form, obj, commit=commit))
- return saved_instances
-
- def save_new_objects(self, commit=True):
- new_objects = []
- for form in self.add_forms:
- if form.is_empty():
- continue
- # If someone has marked an add form for deletion, don't save the
- # object. At some point it would be nice if we didn't display
- # the deletion widget for add forms.
- if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
- continue
- new_objects.append(self.save_new(form, commit=commit))
- return new_objects
-
- def add_fields(self, form, index):
- """Add a hidden field for the object's primary key."""
- self._pk_field_name = self.model._meta.pk.attname
- form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
- super(BaseModelFormSet, self).add_fields(form, index)
-
-def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
- formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
- """
- Returns a FormSet class for the given Django model class. This FormSet
- will contain change forms for every instance of the given model as well
- as the number of add forms specified by ``extra``.
-
- This is essentially the same as ``formset_for_queryset``, but automatically
- uses the model's default manager to determine the queryset.
- """
- form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
- FormSet = formset_for_form(form, formset, extra, orderable, deletable)
- FormSet.model = model
- return FormSet
-
-class InlineFormset(BaseModelFormSet):
- """A formset for child objects related to a parent."""
- def __init__(self, instance, data=None, files=None):
- from django.db.models.fields.related import RelatedObject
- self.instance = instance
- # is there a better way to get the object descriptor?
- self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
- qs = self.get_queryset()
- super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)
-
- def get_queryset(self):
- """
- Returns this FormSet's queryset, but restricted to children of
- self.instance
- """
- kwargs = {self.fk.name: self.instance}
- return self.model._default_manager.filter(**kwargs)
-
- def save_new(self, form, commit=True):
- kwargs = {self.fk.get_attname(): self.instance.pk}
- new_obj = self.model(**kwargs)
- return save_instance(form, new_obj, commit=commit)
-
-def get_foreign_key(parent_model, model, fk_name=None):
- """
- Finds and returns the ForeignKey from model to parent if there is one.
- If fk_name is provided, assume it is the name of the ForeignKey field.
- """
- # avoid circular import
- from django.db.models import ForeignKey
- opts = model._meta
- if fk_name:
- fks_to_parent = [f for f in opts.fields if f.name == fk_name]
- if len(fks_to_parent) == 1:
- fk = fks_to_parent[0]
- if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
- raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
- elif len(fks_to_parent) == 0:
- raise Exception("%s has no field named '%s'" % (model, fk_name))
- else:
- # Try to discover what the ForeignKey from model to parent_model is
- fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
- if len(fks_to_parent) == 1:
- fk = fks_to_parent[0]
- elif len(fks_to_parent) == 0:
- raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
- else:
- raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
- return fk
-
-def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
- """
- Returns an ``InlineFormset`` for the given kwargs.
-
- You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
- to ``parent_model``.
- """
- fk = get_foreign_key(parent_model, model, fk_name=fk_name)
- # let the formset handle object deletion by default
- FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
- formfield_callback=formfield_callback,
- extra=extra, orderable=orderable,
- deletable=deletable)
- # HACK: remove the ForeignKey to the parent from every form
- # This should be done a line above before we pass 'fields' to formset_for_model
- # an 'omit' argument would be very handy here
- try:
- del FormSet.form_class.base_fields[fk.name]
- except KeyError:
- pass
- FormSet.fk = fk
- return FormSet
diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index 18b3fa5eb3..37f8d3a465 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -164,15 +164,19 @@ class Widget(object):
of this widget. Returns None if it's not provided.
"""
return data.get(name, None)
-
- def is_empty(self, value):
+
+ def _has_changed(self, initial, data):
"""
- Given a dictionary of data and this widget's name, return True if the
- widget data is empty or False when not empty.
+ Return True if data differs from initial.
"""
- if value not in (None, ''):
- return False
- return True
+ # For purposes of seeing whether something has changed, None is
+ # the same as an empty string, if the data or inital value we get
+ # is None, replace it w/ u''.
+ data_value = data or u''
+ initial_value = initial or u''
+ if force_unicode(initial_value) != force_unicode(data_value):
+ return True
+ return False
def id_for_label(self, id_):
"""
@@ -309,11 +313,11 @@ class CheckboxInput(Widget):
# send results for unselected checkboxes.
return False
return super(CheckboxInput, self).value_from_datadict(data, files, name)
-
- def is_empty(self, value):
- # this widget will always either be True or False, so always return the
- # opposite value so False values will make the form empty
- return not value
+
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
class Select(Widget):
def __init__(self, attrs=None, choices=()):
@@ -356,12 +360,11 @@ class NullBooleanSelect(Select):
def value_from_datadict(self, data, files, name):
value = data.get(name, None)
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
-
- def is_empty(self, value):
- # this widget will always either be True, False or None, so always
- # return the opposite value so False and None values will make the
- # form empty.
- return not value
+
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
class SelectMultiple(Widget):
def __init__(self, attrs=None, choices=()):
@@ -559,9 +562,11 @@ class MultiWidget(Widget):
def value_from_datadict(self, data, files, name):
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
- def is_empty(self, value):
- for widget, val in zip(self.widgets, value):
- if not widget.is_empty(val):
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = [u'' for x in range(0, len(data))]
+ for widget, initial, data in zip(self.widgets, initial, data):
+ if not widget._has_changed(initial, data):
return False
return True
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
index 19bdeed8f9..386cd04f03 100644
--- a/tests/modeltests/model_formsets/models.py
+++ b/tests/modeltests/model_formsets/models.py
@@ -16,12 +16,12 @@ class Book(models.Model):
__test__ = {'API_TESTS': """
->>> from django.newforms.models import formset_for_model
+>>> from django.newforms.models import _modelformset_factory
>>> qs = Author.objects.all()
->>> AuthorFormSet = formset_for_model(Author, extra=3)
+>>> AuthorFormSet = _modelformset_factory(Author, extra=3)
->>> formset = AuthorFormSet(qs)
+>>> formset = AuthorFormSet(queryset=qs)
>>> for form in formset.forms:
... print form.as_p()
@@ -29,13 +29,14 @@ __test__ = {'API_TESTS': """
>>> data = {
-... 'form-COUNT': '3',
+... 'form-TOTAL_FORMS': '3', # the number of forms rendered
+... 'form-INITIAL_FORMS': '0', # the number of forms with initial data
... 'form-0-name': 'Charles Baudelaire',
... 'form-1-name': 'Arthur Rimbaud',
... 'form-2-name': '',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
@@ -54,9 +55,9 @@ We *could* use formset_for_queryset to restrict the Author objects we edit,
but in that case we'll use it to display them in alphabetical order by name.
>>> qs = Author.objects.order_by('name')
->>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False)
+>>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=False)
->>> formset = AuthorFormSet(qs)
+>>> formset = AuthorFormSet(queryset=qs)
>>> for form in formset.forms:
... print form.as_p()
@@ -65,7 +66,8 @@ but in that case we'll use it to display them in alphabetical order by name.
>>> data = {
-... 'form-COUNT': '3',
+... 'form-TOTAL_FORMS': '3', # the number of forms rendered
+... 'form-INITIAL_FORMS': '2', # the number of forms with initial data
... 'form-0-id': '2',
... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1',
@@ -73,7 +75,7 @@ but in that case we'll use it to display them in alphabetical order by name.
... 'form-2-name': 'Paul Verlaine',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
@@ -91,9 +93,9 @@ This probably shouldn't happen, but it will. If an add form was marked for
deltetion, make sure we don't save that form.
>>> qs = Author.objects.order_by('name')
->>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
+>>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=True)
->>> formset = AuthorFormSet(qs)
+>>> formset = AuthorFormSet(queryset=qs)
>>> for form in formset.forms:
... print form.as_p()
@@ -106,7 +108,8 @@ deltetion, make sure we don't save that form.
>>> data = {
-... 'form-COUNT': '4',
+... 'form-TOTAL_FORMS': '4', # the number of forms rendered
+... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
... 'form-0-id': '2',
... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1',
@@ -117,7 +120,7 @@ deltetion, make sure we don't save that form.
... 'form-3-DELETE': 'on',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
@@ -131,15 +134,17 @@ Charles Baudelaire
Paul Verlaine
+# Inline Formsets ############################################################
+
We can also create a formset that is tied to a parent model. This is how the
admin system's edit inline functionality works.
->>> from django.newforms.models import inline_formset
+>>> from django.newforms.models import _inlineformset_factory
->>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
+>>> AuthorBooksFormSet = _inlineformset_factory(Author, Book, can_delete=False, extra=3)
>>> author = Author.objects.get(name='Charles Baudelaire')
->>> formset = AuthorBooksFormSet(author)
+>>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms:
... print form.as_p()
>>> data = {
-... 'book_set-COUNT': '3',
+... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
+... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': '',
... 'book_set-2-title': '',
... }
->>> formset = AuthorBooksFormSet(author, data=data)
+>>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid()
True
@@ -169,10 +175,10 @@ Now that we've added a book to Charles Baudelaire, let's try adding another
one. This time though, an edit form will be available for every existing
book.
->>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
+>>> AuthorBooksFormSet = _inlineformset_factory(Author, Book, can_delete=False, extra=2)
>>> author = Author.objects.get(name='Charles Baudelaire')
->>> formset = AuthorBooksFormSet(author)
+>>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms:
... print form.as_p()
@@ -180,14 +186,15 @@ book.
>>> data = {
-... 'book_set-COUNT': '3',
+... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
+... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data
... 'book_set-0-id': '1',
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': 'Le Spleen de Paris',
... 'book_set-2-title': '',
... }
->>> formset = AuthorBooksFormSet(author, data=data)
+>>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid()
True
diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
index 7c0cf8abf3..59937bc84e 100644
--- a/tests/regressiontests/forms/forms.py
+++ b/tests/regressiontests/forms/forms.py
@@ -1603,4 +1603,55 @@ the list of errors is empty). You can also use it in {% if %} statements.
+
+
+# The empty_permitted attribute ##############################################
+
+Sometimes (pretty much in formsets) we want to allow a form to pass validation
+if it is completely empty. We can accomplish this by using the empty_permitted
+agrument to a form constructor.
+
+>>> class SongForm(Form):
+... artist = CharField()
+... name = CharField()
+
+First let's show what happens id empty_permitted=False (the default):
+
+>>> data = {'artist': '', 'song': ''}
+
+>>> form = SongForm(data, empty_permitted=False)
+>>> form.is_valid()
+False
+>>> form.errors
+{'name': [u'This field is required.'], 'artist': [u'This field is required.']}
+>>> form.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'SongForm' object has no attribute 'cleaned_data'
+
+
+Now let's show what happens when empty_permitted=True and the form is empty.
+
+>>> form = SongForm(data, empty_permitted=True)
+>>> form.is_valid()
+True
+>>> form.errors
+{}
+>>> form.cleaned_data
+{}
+
+But if we fill in data for one of the fields, the form is no longer empty and
+the whole thing must pass validation.
+
+>>> data = {'artist': 'The Doors', 'song': ''}
+>>> form = SongForm(data, empty_permitted=False)
+>>> form.is_valid()
+False
+>>> form.errors
+{'name': [u'This field is required.']}
+>>> form.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'SongForm' object has no attribute 'cleaned_data'
+
"""
diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py
index a6da2fec36..fea8f057da 100644
--- a/tests/regressiontests/forms/formsets.py
+++ b/tests/regressiontests/forms/formsets.py
@@ -1,38 +1,39 @@
# -*- coding: utf-8 -*-
-formset_tests = """
+tests = """
# Basic FormSet creation and usage ############################################
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_factory function.
>>> from django.newforms import Form, CharField, IntegerField, ValidationError
->>> from django.newforms.formsets import formset_for_form, BaseFormSet
+>>> from django.newforms.formsets import _formset_factory, BaseFormSet
>>> class Choice(Form):
... choice = CharField()
... votes = IntegerField()
->>> ChoiceFormSet = formset_for_form(Choice)
-
+>>> ChoiceFormSet = _formset_factory(Choice)
A FormSet constructor takes the same arguments as Form. Let's create a FormSet
for adding data. By default, it displays 1 blank form. It can display more,
but we'll look at how to do so later.
>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
->>> for form in formset.forms:
-... print form.as_ul()
-
Choice:
-
Votes:
+>>> print formset
+
+
Choice:
+
Votes:
+
On thing to note is that there needs to be a special value in the data. This
value tells the FormSet how many forms were displayed so it can tell how
many forms it needs to clean and validate. You could use javascript to create
new forms on the client side, but they won't get validated unless you increment
-the COUNT field appropriately.
+the TOTAL_FORMS field appropriately.
>>> data = {
-... 'choices-COUNT': '1', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... }
@@ -45,7 +46,7 @@ errors will be a list of dicts rather than just a single dict.
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
+>>> [form.cleaned_data for form in formset.forms]
[{'votes': 100, 'choice': u'Calexico'}]
If a FormSet was not passed any data, its is_valid method should return False.
@@ -57,7 +58,8 @@ FormSet instances can also have an error attribute if validation failed for
any of the forms.
>>> data = {
-... 'choices-COUNT': '1', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '',
... }
@@ -68,13 +70,6 @@ False
>>> formset.errors
[{'votes': [u'This field is required.']}]
-Like a Form instance, cleaned_data won't exist if the formset wasn't validated.
-
->>> formset.cleaned_data
-Traceback (most recent call last):
-...
-AttributeError: 'ChoiceFormSet' object has no attribute 'cleaned_data'
-
We can also prefill a FormSet with existing data by providing an ``initial``
argument to the constructor. ``initial`` should be a list of dicts. By default,
@@ -93,7 +88,8 @@ an extra blank form is included.
Let's simulate what would happen if we submitted this form.
>>> data = {
-... 'choices-COUNT': '2', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': '',
@@ -103,8 +99,8 @@ Let's simulate what would happen if we submitted this form.
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[{'votes': 100, 'choice': u'Calexico'}]
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'choice': u'Calexico'}, {}]
But the second form was blank! Shouldn't we get some errors? No. If we display
a form as blank, it's ok for it to be submitted as blank. If we fill out even
@@ -113,7 +109,8 @@ required that at least x number of forms are completed, but we'll show how to
handle that later.
>>> data = {
-... 'choices-COUNT': '2', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists',
@@ -126,13 +123,13 @@ False
>>> formset.errors
[{}, {'votes': [u'This field is required.']}]
-
If we delete data that was pre-filled, we should get an error. Simply removing
data from form fields isn't the proper way to delete it. We'll see how to
handle that case later.
>>> data = {
-... 'choices-COUNT': '2', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-0-choice': '', # deleted value
... 'choices-0-votes': '', # deleted value
... 'choices-1-choice': '',
@@ -143,15 +140,15 @@ handle that case later.
>>> formset.is_valid()
False
>>> formset.errors
-[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}]
+[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]
# Displaying more than 1 blank form ###########################################
We can also display more than 1 empty form at a time. To do so, pass a
-num_extra argument to formset_for_form.
+extra argument to _formset_factory.
->>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)
+>>> ChoiceFormSet = _formset_factory(Choice, extra=3)
>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms:
@@ -168,7 +165,8 @@ This may seem a little strange, but later we will show how to require a minimum
number of forms to be completed.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': '',
... 'choices-0-votes': '',
... 'choices-1-choice': '',
@@ -180,14 +178,15 @@ number of forms to be completed.
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[]
+>>> [form.cleaned_data for form in formset.forms]
+[{}, {}, {}]
We can just fill out one of the forms.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': '',
@@ -199,14 +198,15 @@ We can just fill out one of the forms.
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[{'votes': 100, 'choice': u'Calexico'}]
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'choice': u'Calexico'}, {}, {}]
And once again, if we try to partially complete a form, validation will fail.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists',
@@ -219,10 +219,10 @@ And once again, if we try to partially complete a form, validation will fail.
>>> formset.is_valid()
False
>>> formset.errors
-[{}, {'votes': [u'This field is required.']}]
+[{}, {'votes': [u'This field is required.']}, {}]
-The num_extra argument also works when the formset is pre-filled with initial
+The extra argument also works when the formset is pre-filled with initial
data.
>>> initial = [{'choice': u'Calexico', 'votes': 100}]
@@ -239,36 +239,13 @@ data.
Votes:
-If we try to skip a form, even if it was initially displayed as blank, we will
-get an error.
-
->>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
-... 'choices-0-choice': 'Calexico',
-... 'choices-0-votes': '100',
-... 'choices-1-choice': '',
-... 'choices-1-votes': '',
-... 'choices-2-choice': 'The Decemberists',
-... 'choices-2-votes': '12',
-... 'choices-3-choice': '',
-... 'choices-3-votes': '',
-... }
-
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
->>> formset.is_valid()
-False
->>> formset.errors
-[{}, {'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]
-
-
# FormSets with deletion ######################################################
We can easily add deletion ability to a FormSet with an agrument to
-formset_for_form. This will add a boolean field to each form instance. When
-that boolean field is True, the cleaned data will be in formset.deleted_data
-rather than formset.cleaned_data
+_formset_factory. This will add a boolean field to each form instance. When
+that boolean field is True, the form will be in formset.deleted_forms
->>> ChoiceFormSet = formset_for_form(Choice, deletable=True)
+>>> ChoiceFormSet = _formset_factory(Choice, can_delete=True)
>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
@@ -288,7 +265,8 @@ To delete something, we just need to set that form's special delete field to
'on'. Let's go ahead and delete Fergie.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-DELETE': '',
@@ -303,22 +281,23 @@ To delete something, we just need to set that form's special delete field to
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}]
->>> formset.deleted_data
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}]
+>>> [form.cleaned_data for form in formset.deleted_forms]
[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]
+
# FormSets with ordering ######################################################
We can also add ordering ability to a FormSet with an agrument to
-formset_for_form. This will add a integer field to each form instance. When
-form validation succeeds, formset.cleaned_data will have the data in the correct
+_formset_factory. This will add a integer field to each form instance. When
+form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct
order specified by the ordering fields. If a number is duplicated in the set
of ordering fields, for instance form 0 and form 3 are both marked as 1, then
the form index used as a secondary ordering criteria. In order to put
something at the front of the list, you'd need to set it's order to 0.
->>> ChoiceFormSet = formset_for_form(Choice, orderable=True)
+>>> ChoiceFormSet = _formset_factory(Choice, can_order=True)
>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
@@ -332,10 +311,11 @@ something at the front of the list, you'd need to set it's order to 0.
Order:
Choice:
Votes:
-
Order:
+
Order:
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@@ -350,17 +330,48 @@ something at the front of the list, you'd need to set it's order to 0.
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> for cleaned_data in formset.cleaned_data:
-... print cleaned_data
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}
+Ordering fields are allowed to be left blank, and if they *are* left blank,
+they will be sorted below everything else.
+
+>>> data = {
+... 'choices-TOTAL_FORMS': '4', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
+... 'choices-0-choice': 'Calexico',
+... 'choices-0-votes': '100',
+... 'choices-0-ORDER': '1',
+... 'choices-1-choice': 'Fergie',
+... 'choices-1-votes': '900',
+... 'choices-1-ORDER': '2',
+... 'choices-2-choice': 'The Decemberists',
+... 'choices-2-votes': '500',
+... 'choices-2-ORDER': '',
+... 'choices-3-choice': 'Basia Bulat',
+... 'choices-3-votes': '50',
+... 'choices-3-ORDER': '',
+... }
+
+>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset.is_valid()
+True
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
+{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
+{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}
+{'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'}
+{'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'}
+
+
# FormSets with ordering + deletion ###########################################
Let's try throwing ordering and deletion into the same form.
->>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)
+>>> ChoiceFormSet = _formset_factory(Choice, can_order=True, can_delete=True)
>>> initial = [
... {'choice': u'Calexico', 'votes': 100},
@@ -384,13 +395,14 @@ Let's try throwing ordering and deletion into the same form.
Delete:
Choice:
Votes:
-
Order:
+
Order:
Delete:
Let's delete Fergie, and put The Decemberists ahead of Calexico.
>>> data = {
-... 'choices-COUNT': '4', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '4', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@@ -405,18 +417,18 @@ Let's delete Fergie, and put The Decemberists ahead of Calexico.
... 'choices-2-DELETE': '',
... 'choices-3-choice': '',
... 'choices-3-votes': '',
-... 'choices-3-ORDER': '4',
+... 'choices-3-ORDER': '',
... 'choices-3-DELETE': '',
... }
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> for cleaned_data in formset.cleaned_data:
-... print cleaned_data
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'}
->>> formset.deleted_data
+>>> [form.cleaned_data for form in formset.deleted_forms]
[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]
@@ -433,10 +445,10 @@ error if there are any duplicates.
...
>>> class FavoriteDrinksFormSet(BaseFormSet):
-... form_class = FavoriteDrinkForm
-... num_extra = 2
-... orderable = False
-... deletable = False
+... form = FavoriteDrinkForm
+... extra = 2
+... can_order = False
+... can_delete = False
...
... def clean(self):
... seen_drinks = []
@@ -444,13 +456,13 @@ error if there are any duplicates.
... 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-TOTAL_FORMS': '2', # the number of forms rendered
+... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
... 'drinks-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Gin and Tonic',
... }
@@ -470,7 +482,8 @@ You may only specify a drink once.
Make sure we didn't break the valid case.
>>> data = {
-... 'drinks-COUNT': '2',
+... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
+... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
... 'drinks-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Bloody Mary',
... }
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index f854338814..ff8213c8d9 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -26,7 +26,7 @@ from localflavor.za import tests as localflavor_za_tests
from regressions import tests as regression_tests
from util import tests as util_tests
from widgets import tests as widgets_tests
-from formsets import formset_tests
+from formsets import tests as formset_tests
from media import media_tests
__test__ = {
diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py
index ccfddc9fcd..0e69602103 100644
--- a/tests/regressiontests/forms/widgets.py
+++ b/tests/regressiontests/forms/widgets.py
@@ -292,12 +292,6 @@ checkboxes).
>>> w.value_from_datadict({}, {}, 'testing')
False
-The CheckboxInput widget will always be empty when there is a False value
->>> w.is_empty(False)
-True
->>> w.is_empty(True)
-False
-
# Select Widget ###############################################################
>>> w = Select()
@@ -459,15 +453,6 @@ over multiple times without getting consumed:
-The NullBooleanSelect widget will always be empty when Unknown or No is selected
-as its value. This is to stay compliant with the CheckboxInput behavior
->>> w.is_empty(False)
-True
->>> w.is_empty(None)
-True
->>> w.is_empty(True)
-False
-
""" + \
r""" # [This concatenation is to keep the string below the jython's 32K limit].
# SelectMultiple Widget #######################################################
@@ -910,16 +895,6 @@ u' <
>>> w.render('name', ['john', 'lennon'])
u' '
-The MultiWidget will be empty only when all widgets are considered empty.
->>> w.is_empty(['john', 'lennon'])
-False
->>> w.is_empty(['john', ''])
-False
->>> w.is_empty(['', ''])
-True
->>> w.is_empty([None, None])
-True
-
# SplitDateTimeWidget #########################################################
>>> w = SplitDateTimeWidget()
diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py
index f84be84c0d..6f197d6ab5 100644
--- a/tests/regressiontests/inline_formsets/models.py
+++ b/tests/regressiontests/inline_formsets/models.py
@@ -15,13 +15,13 @@ class Child(models.Model):
__test__ = {'API_TESTS': """
->>> from django.newforms.models import inline_formset
+>>> from django.newforms.models import _inlineformset_factory
Child has two ForeignKeys to Parent, so if we don't specify which one to use
for the inline formset, we should get an exception.
->>> ifs = inline_formset(Parent, Child)
+>>> ifs = _inlineformset_factory(Parent, Child)
Traceback (most recent call last):
...
Exception: has more than 1 ForeignKey to
@@ -29,14 +29,14 @@ Exception: has more than
These two should both work without a problem.
->>> ifs = inline_formset(Parent, Child, fk_name='mother')
->>> ifs = inline_formset(Parent, Child, fk_name='father')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='mother')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='father')
If we specify fk_name, but it isn't a ForeignKey from the child model to the
parent model, we should get an exception.
->>> ifs = inline_formset(Parent, Child, fk_name='school')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='school')
Traceback (most recent call last):
...
Exception: fk_name 'school' is not a ForeignKey to
@@ -45,7 +45,7 @@ Exception: fk_name 'school' is not a ForeignKey to >> ifs = inline_formset(Parent, Child, fk_name='test')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='test')
Traceback (most recent call last):
...
Exception: has no field named 'test'