1
0
mirror of https://github.com/django/django.git synced 2025-07-04 01:39:20 +00:00

newforms-admin: Cleaned up the implementation and APIs of all the formset classes. Backwards-incompatible.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7270 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Joseph Kocherhans 2008-03-17 17:55:16 +00:00
parent 40d55238b8
commit 93c45b5704
11 changed files with 623 additions and 504 deletions

View File

@ -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):
"""

View File

@ -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):
"""

View File

@ -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,12 +19,14 @@ 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."""
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
@ -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):
def __unicode__(self):
return self.as_table()
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:
if self.data or self.files:
kwargs['data'] = self.data
if self.files:
kwargs['files'] = self.files
if self.initial:
try:
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)
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 _forms(self):
return self.change_forms + self.add_forms
forms = property(_forms)
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,23 +219,28 @@ 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
@ -193,9 +251,18 @@ class BaseFormSet(object):
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 <tr>s -- excluding the <table></table>."
# 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):

View File

@ -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

View File

@ -165,14 +165,18 @@ class Widget(object):
"""
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
# 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_):
"""
@ -310,10 +314,10 @@ class CheckboxInput(Widget):
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=()):
@ -357,11 +361,10 @@ class NullBooleanSelect(Select):
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

View File

@ -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()
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
@ -29,13 +29,14 @@ __test__ = {'API_TESTS': """
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
>>> 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()
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></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()
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>
@ -106,7 +108,8 @@ deltetion, make sure we don't save that form.
<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></p>
>>> 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()
<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
@ -147,13 +152,14 @@ admin system's edit inline functionality works.
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></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()
<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
@ -180,14 +186,15 @@ book.
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
>>> 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

View File

@ -1603,4 +1603,55 @@ the list of errors is empty). You can also use it in {% if %} statements.
<p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p>
<input type="submit" />
</form>
# 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'
"""

View File

@ -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()
<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="text" name="choices-0-votes" /></li>
>>> print formset
<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>
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.
<li>Votes: <input type="text" name="choices-3-votes" /></li>
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.
<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
<li>Order: <input type="text" name="choices-2-ORDER" /></li>
>>> 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.
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></li>
<li>Order: <input type="text" name="choices-3-ORDER" value="4" /></li>
<li>Order: <input type="text" name="choices-3-ORDER" /></li>
<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>
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',
... }

View File

@ -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__ = {

View File

@ -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:
<option value="3" selected="selected">No</option>
</select>
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'<input id="foo_0" type="text" class="big" value="john" name="name_0" /><br /><
>>> w.render('name', ['john', 'lennon'])
u'<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />'
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()

View File

@ -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: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
@ -29,14 +29,14 @@ Exception: <class 'regressiontests.inline_formsets.models.Child'> 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 <class 'regressiontests.inline_formsets.models.Parent'>
@ -45,7 +45,7 @@ Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inlin
If the field specified in fk_name is not a ForeignKey, we should get an
exception.
>>> ifs = inline_formset(Parent, Child, fk_name='test')
>>> ifs = _inlineformset_factory(Parent, Child, fk_name='test')
Traceback (most recent call last):
...
Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'