1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +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 oldforms, template
from django import newforms as forms from django import newforms as forms
from django.newforms.formsets import all_valid 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.contenttypes.models import ContentType
from django.contrib.admin import widgets from django.contrib.admin import widgets
from django.contrib.admin.util import get_deleted_objects from django.contrib.admin.util import get_deleted_objects
@ -340,7 +341,7 @@ class ModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets) fields = flatten_fieldsets(self.declared_fieldsets)
else: else:
fields = None 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): def form_change(self, request, obj):
""" """
@ -350,7 +351,7 @@ class ModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets) fields = flatten_fieldsets(self.declared_fieldsets)
else: else:
fields = None 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): def save_add(self, request, model, form, formsets, post_url_continue):
""" """
@ -496,14 +497,14 @@ class ModelAdmin(BaseModelAdmin):
if request.method == 'POST': if request.method == 'POST':
form = ModelForm(request.POST, request.FILES) form = ModelForm(request.POST, request.FILES)
for FormSet in self.formsets_add(request): 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) inline_formsets.append(inline_formset)
if all_valid(inline_formsets) and form.is_valid(): if all_valid(inline_formsets) and form.is_valid():
return self.save_add(request, model, form, inline_formsets, '../%s/') return self.save_add(request, model, form, inline_formsets, '../%s/')
else: else:
form = ModelForm(initial=request.GET) form = ModelForm(initial=request.GET)
for FormSet in self.formsets_add(request): for FormSet in self.formsets_add(request):
inline_formset = FormSet(obj) inline_formset = FormSet(instance=obj)
inline_formsets.append(inline_formset) inline_formsets.append(inline_formset)
adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields) adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
@ -553,17 +554,17 @@ class ModelAdmin(BaseModelAdmin):
ModelForm = self.form_change(request, obj) ModelForm = self.form_change(request, obj)
inline_formsets = [] inline_formsets = []
if request.method == 'POST': 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): 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) inline_formsets.append(inline_formset)
if all_valid(inline_formsets) and form.is_valid(): if all_valid(inline_formsets) and form.is_valid():
return self.save_change(request, model, form, inline_formsets) return self.save_change(request, model, form, inline_formsets)
else: else:
form = ModelForm() form = ModelForm(instance=obj)
for FormSet in self.formsets_change(request, obj): for FormSet in self.formsets_change(request, obj):
inline_formset = FormSet(obj) inline_formset = FormSet(instance=obj)
inline_formsets.append(inline_formset) inline_formsets.append(inline_formset)
## Populate the FormWrapper. ## Populate the FormWrapper.
@ -740,7 +741,7 @@ class InlineModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets) fields = flatten_fieldsets(self.declared_fieldsets)
else: else:
fields = None 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): def formset_change(self, request, obj):
"""Returns an InlineFormSet class for use in admin change views.""" """Returns an InlineFormSet class for use in admin change views."""
@ -748,18 +749,18 @@ class InlineModelAdmin(BaseModelAdmin):
fields = flatten_fieldsets(self.declared_fieldsets) fields = flatten_fieldsets(self.declared_fieldsets)
else: else:
fields = None 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): def fieldsets_add(self, request):
if self.declared_fieldsets: if self.declared_fieldsets:
return 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()})] return [(None, {'fields': form.base_fields.keys()})]
def fieldsets_change(self, request, obj): def fieldsets_change(self, request, obj):
if self.declared_fieldsets: if self.declared_fieldsets:
return 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()})] return [(None, {'fields': form.base_fields.keys()})]
class StackedInline(InlineModelAdmin): class StackedInline(InlineModelAdmin):
@ -778,14 +779,14 @@ class InlineAdminFormSet(object):
self.fieldsets = fieldsets self.fieldsets = fieldsets
def __iter__(self): 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) 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) yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
def fields(self): def fields(self):
for field_name in flatten_fieldsets(self.fieldsets): 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): class InlineAdminForm(AdminForm):
""" """

View File

@ -69,7 +69,8 @@ class BaseForm(StrAndUnicode):
# information. Any improvements to the form API should be made to *this* # information. Any improvements to the form API should be made to *this*
# class, not to the Form class. # class, not to the Form class.
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 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.is_bound = data is not None or files is not None
self.data = data or {} self.data = data or {}
self.files = files or {} self.files = files or {}
@ -78,6 +79,7 @@ class BaseForm(StrAndUnicode):
self.initial = initial or {} self.initial = initial or {}
self.error_class = error_class self.error_class = error_class
self.label_suffix = label_suffix self.label_suffix = label_suffix
self.empty_permitted = empty_permitted
self._errors = None # Stores the errors after clean() has been called. self._errors = None # Stores the errors after clean() has been called.
# The base_fields class attribute is the *class-wide* definition of # 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()) 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): def full_clean(self):
""" """
Cleans all of self.data and populates self._errors and 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. if not self.is_bound: # Stop further processing.
return return
self.cleaned_data = {} 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(): for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries. # value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some # Each widget type knows how to retrieve its own data, because some
@ -251,11 +239,24 @@ class BaseForm(StrAndUnicode):
""" """
return self.cleaned_data return self.cleaned_data
def reset(self): def has_changed(self):
"""Return this form to the state it was in before data was passed to it.""" """
self.data = {} Returns True if data differs from initial.
self.is_bound = False """
self.__errors = None # 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): def _get_media(self):
""" """

View File

@ -1,12 +1,14 @@
from forms import Form from forms import Form
from django.utils.encoding import StrAndUnicode
from fields import IntegerField, BooleanField from fields import IntegerField, BooleanField
from widgets import HiddenInput, Media from widgets import HiddenInput, TextInput
from util import ErrorList, ValidationError from util import ErrorList, ValidationError
__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid') __all__ = ('BaseFormSet', 'all_valid')
# special field names # special field names
FORM_COUNT_FIELD_NAME = 'COUNT' TOTAL_FORM_COUNT = 'TOTAL_FORMS'
INITIAL_FORM_COUNT = 'INITIAL_FORMS'
ORDERING_FIELD_NAME = 'ORDER' ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE' DELETION_FIELD_NAME = 'DELETE'
@ -17,12 +19,14 @@ class ManagementForm(Form):
increment the count field of this form as well. increment the count field of this form as well.
""" """
def __init__(self, *args, **kwargs): 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) super(ManagementForm, self).__init__(*args, **kwargs)
class BaseFormSet(object): class BaseFormSet(StrAndUnicode):
"""A collection of instances of the same Form class.""" """
A collection of instances of the same Form class.
"""
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList): initial=None, error_class=ErrorList):
self.is_bound = data is not None or files is not None self.is_bound = data is not None or files is not None
@ -32,68 +36,132 @@ class BaseFormSet(object):
self.files = files self.files = files
self.initial = initial self.initial = initial
self.error_class = error_class 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 # initialization is different depending on whether we recieved data, initial, or nothing
if data or files: if data or files:
self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
if self.management_form.is_valid(): if self.management_form.is_valid():
self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME] self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
self.required_forms = self.total_forms - self.num_extra self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
self.change_form_count = self.total_forms - self.num_extra
else: else:
# not sure that ValidationError is the best thing to raise here
raise ValidationError('ManagementForm data is missing or has been tampered with') raise ValidationError('ManagementForm data is missing or has been tampered with')
elif initial: elif initial:
self.change_form_count = len(initial) self._initial_form_count = len(initial)
self.required_forms = len(initial) self._total_form_count = self._initial_form_count + self.extra
self.total_forms = self.required_forms + self.num_extra
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
else: else:
self.change_form_count = 0 self._initial_form_count = 0
self.required_forms = 0 self._total_form_count = self.extra
self.total_forms = self.num_extra initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count}
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) self.management_form = ManagementForm(initial=initial, auto_id=auto_id, prefix=prefix)
def _get_add_forms(self): # instantiate all the forms and put them in self.forms
"""Return a list of all the add forms in this ``FormSet``.""" self.forms = []
FormClass = self.form_class for i in range(self._total_form_count):
if not hasattr(self, '_add_forms'): self.forms.append(self._construct_form(i))
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)
def _get_change_forms(self): def __unicode__(self):
"""Return a list of all the change forms in this ``FormSet``.""" return self.as_table()
FormClass = self.form_class
if not hasattr(self, '_change_forms'): def _construct_form(self, i):
change_forms = [] """
for i in range(0, self.change_form_count): Instantiates and returns the i-th form instance in a formset.
"""
kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
if self.data: if self.data or self.files:
kwargs['data'] = self.data kwargs['data'] = self.data
if self.files:
kwargs['files'] = self.files kwargs['files'] = self.files
if self.initial: if self.initial:
try:
kwargs['initial'] = self.initial[i] kwargs['initial'] = self.initial[i]
change_form = FormClass(**kwargs) except IndexError:
self.add_fields(change_form, i) pass
change_forms.append(change_form) # Allow extra forms to be empty.
self._change_forms= change_forms if i >= self._initial_form_count:
return self._change_forms kwargs['empty_permitted'] = True
change_forms = property(_get_change_forms) form = self.form(**kwargs)
self.add_fields(form, i)
return form
def _forms(self): def _get_initial_forms(self):
return self.change_forms + self.add_forms """Return a list of all the intial forms in this formset."""
forms = property(_forms) 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): def non_form_errors(self):
""" """
@ -101,63 +169,48 @@ class BaseFormSet(object):
form -- i.e., from formset.clean(). Returns an empty ErrorList if there form -- i.e., from formset.clean(). Returns an empty ErrorList if there
are none. are none.
""" """
if hasattr(self, '_non_form_errors'): if self._non_form_errors is not None:
return self._non_form_errors return self._non_form_errors
return self.error_class() 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): 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. Cleans all of self.data and populates self._errors.
errors = [] """
self._errors = []
if not self.is_bound: # Stop further processing. if not self.is_bound: # Stop further processing.
self.__errors = errors
return return
self.cleaned_data = [] for i in range(0, self._total_form_count):
self.deleted_data = [] form = self.forms[i]
# Process change forms self._errors.append(form.errors)
for form in self.change_forms: # Give self.clean() a chance to do cross-form validation.
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
try: try:
self.cleaned_data = self.clean() self.clean()
except ValidationError, e: except ValidationError, e:
self._non_form_errors = e.messages 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): def clean(self):
""" """
@ -166,23 +219,28 @@ class BaseFormSet(object):
will not be associated with a particular form; it will be accesible will not be associated with a particular form; it will be accesible
via formset.non_form_errors() via formset.non_form_errors()
""" """
return self.cleaned_data pass
def add_fields(self, form, index): def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance.""" """A hook for adding extra fields on to each form instance."""
if self.orderable: if self.can_order:
form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) # Only pre-fill the ordering field for initial forms.
if self.deletable: 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) form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
def add_prefix(self, index): def add_prefix(self, index):
return '%s-%s' % (self.prefix, index) return '%s-%s' % (self.prefix, index)
def is_valid(self): def is_multipart(self):
if not self.is_bound: """
return False Returns True if the formset needs to be multipart-encrypted, i.e. it
self.full_clean() has FileInput. Otherwise, False.
return self._is_valid """
return self.forms[0].is_multipart()
def _get_media(self): 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
@ -193,9 +251,18 @@ class BaseFormSet(object):
return Media() return Media()
media = property(_get_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.""" """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) return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets): def all_valid(formsets):

View File

@ -13,13 +13,12 @@ from django.core.exceptions import ImproperlyConfigured
from util import ValidationError, ErrorList from util import ValidationError, ErrorList
from forms import BaseForm, get_declared_fields from forms import BaseForm, get_declared_fields
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 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 widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
from formsets import BaseFormSet, _formset_factory, DELETION_FIELD_NAME
__all__ = ( __all__ = (
'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
'formset_for_model', 'inline_formset',
'ModelChoiceField', 'ModelMultipleChoiceField', 'ModelChoiceField', 'ModelMultipleChoiceField',
) )
@ -245,7 +244,7 @@ class ModelFormMetaclass(type):
class BaseModelForm(BaseForm): class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 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=':',
instance=None): empty_permitted=False, instance=None):
opts = self._meta opts = self._meta
if instance is None: if instance is None:
# if we didn't get an instance, instantiate a new one # 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 was provided, it should override the values from instance
if initial is not None: if initial is not None:
object_data.update(initial) 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): def save(self, commit=True):
""" """
@ -276,6 +276,182 @@ class BaseModelForm(BaseForm):
class ModelForm(BaseModelForm): class ModelForm(BaseModelForm):
__metaclass__ = ModelFormMetaclass __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 ##################################################################### # Fields #####################################################################
@ -387,180 +563,3 @@ class ModelMultipleChoiceField(ModelChoiceField):
else: else:
final_values.append(obj) final_values.append(obj)
return final_values 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) 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 Return True if data differs from initial.
widget data is empty or False when not empty.
""" """
if value not in (None, ''): # For purposes of seeing whether something has changed, None is
return False # 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 True
return False
def id_for_label(self, id_): def id_for_label(self, id_):
""" """
@ -310,10 +314,10 @@ class CheckboxInput(Widget):
return False return False
return super(CheckboxInput, self).value_from_datadict(data, files, name) return super(CheckboxInput, self).value_from_datadict(data, files, name)
def is_empty(self, value): def _has_changed(self, initial, data):
# this widget will always either be True or False, so always return the # Sometimes data or initial could be None or u'' which should be the
# opposite value so False values will make the form empty # same thing as False.
return not value return bool(initial) != bool(data)
class Select(Widget): class Select(Widget):
def __init__(self, attrs=None, choices=()): def __init__(self, attrs=None, choices=()):
@ -357,11 +361,10 @@ class NullBooleanSelect(Select):
value = data.get(name, None) value = data.get(name, None)
return {u'2': True, u'3': False, True: True, False: False}.get(value, None) return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
def is_empty(self, value): def _has_changed(self, initial, data):
# this widget will always either be True, False or None, so always # Sometimes data or initial could be None or u'' which should be the
# return the opposite value so False and None values will make the # same thing as False.
# form empty. return bool(initial) != bool(data)
return not value
class SelectMultiple(Widget): class SelectMultiple(Widget):
def __init__(self, attrs=None, choices=()): def __init__(self, attrs=None, choices=()):
@ -559,9 +562,11 @@ class MultiWidget(Widget):
def value_from_datadict(self, data, files, name): 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)] return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
def is_empty(self, value): def _has_changed(self, initial, data):
for widget, val in zip(self.widgets, value): if initial is None:
if not widget.is_empty(val): 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 False
return True return True

View File

@ -16,12 +16,12 @@ class Book(models.Model):
__test__ = {'API_TESTS': """ __test__ = {'API_TESTS': """
>>> from django.newforms.models import formset_for_model >>> from django.newforms.models import _modelformset_factory
>>> qs = Author.objects.all() >>> 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: >>> for form in formset.forms:
... print form.as_p() ... 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> <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> <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 = { >>> 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-0-name': 'Charles Baudelaire',
... 'form-1-name': 'Arthur Rimbaud', ... 'form-1-name': 'Arthur Rimbaud',
... 'form-2-name': '', ... 'form-2-name': '',
... } ... }
>>> formset = AuthorFormSet(qs, data=data) >>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid() >>> formset.is_valid()
True 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. but in that case we'll use it to display them in alphabetical order by name.
>>> qs = Author.objects.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: >>> for form in formset.forms:
... print form.as_p() ... 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> <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 = { >>> 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-id': '2',
... 'form-0-name': 'Arthur Rimbaud', ... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1', ... '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', ... 'form-2-name': 'Paul Verlaine',
... } ... }
>>> formset = AuthorFormSet(qs, data=data) >>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid() >>> formset.is_valid()
True 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. deltetion, make sure we don't save that form.
>>> qs = Author.objects.order_by('name') >>> 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: >>> for form in formset.forms:
... print form.as_p() ... 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> <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> <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 = { >>> 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-id': '2',
... 'form-0-name': 'Arthur Rimbaud', ... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1', ... 'form-1-id': '1',
@ -117,7 +120,7 @@ deltetion, make sure we don't save that form.
... 'form-3-DELETE': 'on', ... 'form-3-DELETE': 'on',
... } ... }
>>> formset = AuthorFormSet(qs, data=data) >>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid() >>> formset.is_valid()
True True
@ -131,15 +134,17 @@ Charles Baudelaire
Paul Verlaine Paul Verlaine
# Inline Formsets ############################################################
We can also create a formset that is tied to a parent model. This is how the We can also create a formset that is tied to a parent model. This is how the
admin system's edit inline functionality works. 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') >>> author = Author.objects.get(name='Charles Baudelaire')
>>> formset = AuthorBooksFormSet(author) >>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms: >>> for form in formset.forms:
... print form.as_p() ... 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> <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> <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 = { >>> 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-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': '', ... 'book_set-1-title': '',
... 'book_set-2-title': '', ... 'book_set-2-title': '',
... } ... }
>>> formset = AuthorBooksFormSet(author, data=data) >>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid() >>> formset.is_valid()
True 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 one. This time though, an edit form will be available for every existing
book. 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') >>> author = Author.objects.get(name='Charles Baudelaire')
>>> formset = AuthorBooksFormSet(author) >>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms: >>> for form in formset.forms:
... print form.as_p() ... 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> <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> <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 = { >>> 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-id': '1',
... 'book_set-0-title': 'Les Fleurs du Mal', ... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': 'Le Spleen de Paris', ... 'book_set-1-title': 'Le Spleen de Paris',
... 'book_set-2-title': '', ... 'book_set-2-title': '',
... } ... }
>>> formset = AuthorBooksFormSet(author, data=data) >>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid() >>> formset.is_valid()
True 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> <p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p>
<input type="submit" /> <input type="submit" />
</form> </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 -*- # -*- coding: utf-8 -*-
formset_tests = """ tests = """
# Basic FormSet creation and usage ############################################ # Basic FormSet creation and usage ############################################
FormSet allows us to use multiple instance of the same form on 1 page. For now, FormSet allows us to use multiple instance of the same form on 1 page. For now,
the best way to create a FormSet is by using the formset_for_form function. the best way to create a FormSet is by using the _formset_factory function.
>>> from django.newforms import Form, CharField, IntegerField, ValidationError >>> 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): >>> class Choice(Form):
... choice = CharField() ... choice = CharField()
... votes = IntegerField() ... 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 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, for adding data. By default, it displays 1 blank form. It can display more,
but we'll look at how to do so later. but we'll look at how to do so later.
>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms: >>> print formset
... print form.as_ul() <input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" />
<li>Choice: <input type="text" name="choices-0-choice" /></li> <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
<li>Votes: <input type="text" name="choices-0-votes" /></li> <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 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 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 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 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 = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... '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 = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> formset.cleaned_data >>> [form.cleaned_data for form in formset.forms]
[{'votes': 100, 'choice': u'Calexico'}] [{'votes': 100, 'choice': u'Calexico'}]
If a FormSet was not passed any data, its is_valid method should return False. 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. any of the forms.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '', ... 'choices-0-votes': '',
... } ... }
@ -68,13 +70,6 @@ False
>>> formset.errors >>> formset.errors
[{'votes': [u'This field is required.']}] [{'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`` 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, 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. Let's simulate what would happen if we submitted this form.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-1-choice': '', ... '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 = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> formset.cleaned_data >>> [form.cleaned_data for form in formset.forms]
[{'votes': 100, 'choice': u'Calexico'}] [{'votes': 100, 'choice': u'Calexico'}, {}]
But the second form was blank! Shouldn't we get some errors? No. If we display 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 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. handle that later.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists', ... 'choices-1-choice': 'The Decemberists',
@ -126,13 +123,13 @@ False
>>> formset.errors >>> formset.errors
[{}, {'votes': [u'This field is required.']}] [{}, {'votes': [u'This field is required.']}]
If we delete data that was pre-filled, we should get an error. Simply removing 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 data from form fields isn't the proper way to delete it. We'll see how to
handle that case later. handle that case later.
>>> data = { >>> 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-choice': '', # deleted value
... 'choices-0-votes': '', # deleted value ... 'choices-0-votes': '', # deleted value
... 'choices-1-choice': '', ... 'choices-1-choice': '',
@ -143,15 +140,15 @@ handle that case later.
>>> formset.is_valid() >>> formset.is_valid()
False False
>>> formset.errors >>> 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 ########################################### # Displaying more than 1 blank form ###########################################
We can also display more than 1 empty form at a time. To do so, pass a 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') >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms: >>> 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. number of forms to be completed.
>>> data = { >>> 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-choice': '',
... 'choices-0-votes': '', ... 'choices-0-votes': '',
... 'choices-1-choice': '', ... 'choices-1-choice': '',
@ -180,14 +178,15 @@ number of forms to be completed.
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> formset.cleaned_data >>> [form.cleaned_data for form in formset.forms]
[] [{}, {}, {}]
We can just fill out one of the forms. We can just fill out one of the forms.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-1-choice': '', ... '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 = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> formset.cleaned_data >>> [form.cleaned_data for form in formset.forms]
[{'votes': 100, 'choice': u'Calexico'}] [{'votes': 100, 'choice': u'Calexico'}, {}, {}]
And once again, if we try to partially complete a form, validation will fail. And once again, if we try to partially complete a form, validation will fail.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists', ... '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() >>> formset.is_valid()
False False
>>> formset.errors >>> 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. data.
>>> initial = [{'choice': u'Calexico', 'votes': 100}] >>> initial = [{'choice': u'Calexico', 'votes': 100}]
@ -239,36 +239,13 @@ data.
<li>Votes: <input type="text" name="choices-3-votes" /></li> <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 ###################################################### # FormSets with deletion ######################################################
We can easily add deletion ability to a FormSet with an agrument to 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 _formset_factory. 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 that boolean field is True, the form will be in formset.deleted_forms
rather than formset.cleaned_data
>>> 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}] >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') >>> 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. 'on'. Let's go ahead and delete Fergie.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-0-DELETE': '', ... '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 = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> formset.cleaned_data >>> [form.cleaned_data for form in formset.forms]
[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}] [{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}]
>>> formset.deleted_data >>> [form.cleaned_data for form in formset.deleted_forms]
[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}] [{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]
# FormSets with ordering ###################################################### # FormSets with ordering ######################################################
We can also add ordering ability to a FormSet with an agrument to 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 _formset_factory. This will add a integer field to each form instance. When
form validation succeeds, formset.cleaned_data will have the data in the correct 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 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 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 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. 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}] >>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') >>> 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>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li> <li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></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 = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1', ... '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 = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> for cleaned_data in formset.cleaned_data: >>> for form in formset.ordered_forms:
... print cleaned_data ... print form.cleaned_data
{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'} {'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} {'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 ########################################### # FormSets with ordering + deletion ###########################################
Let's try throwing ordering and deletion into the same form. 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 = [ >>> initial = [
... {'choice': u'Calexico', 'votes': 100}, ... {'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>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li> <li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></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> <li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>
Let's delete Fergie, and put The Decemberists ahead of Calexico. Let's delete Fergie, and put The Decemberists ahead of Calexico.
>>> data = { >>> 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-choice': 'Calexico',
... 'choices-0-votes': '100', ... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1', ... 'choices-0-ORDER': '1',
@ -405,18 +417,18 @@ Let's delete Fergie, and put The Decemberists ahead of Calexico.
... 'choices-2-DELETE': '', ... 'choices-2-DELETE': '',
... 'choices-3-choice': '', ... 'choices-3-choice': '',
... 'choices-3-votes': '', ... 'choices-3-votes': '',
... 'choices-3-ORDER': '4', ... 'choices-3-ORDER': '',
... 'choices-3-DELETE': '', ... 'choices-3-DELETE': '',
... } ... }
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid() >>> formset.is_valid()
True True
>>> for cleaned_data in formset.cleaned_data: >>> for form in formset.ordered_forms:
... print cleaned_data ... print form.cleaned_data
{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'} {'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'} {'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'}] [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]
@ -433,10 +445,10 @@ error if there are any duplicates.
... ...
>>> class FavoriteDrinksFormSet(BaseFormSet): >>> class FavoriteDrinksFormSet(BaseFormSet):
... form_class = FavoriteDrinkForm ... form = FavoriteDrinkForm
... num_extra = 2 ... extra = 2
... orderable = False ... can_order = False
... deletable = False ... can_delete = False
... ...
... def clean(self): ... def clean(self):
... seen_drinks = [] ... seen_drinks = []
@ -444,13 +456,13 @@ error if there are any duplicates.
... if drink['name'] in seen_drinks: ... if drink['name'] in seen_drinks:
... raise ValidationError('You may only specify a drink once.') ... raise ValidationError('You may only specify a drink once.')
... seen_drinks.append(drink['name']) ... seen_drinks.append(drink['name'])
... return self.cleaned_data
... ...
We start out with a some duplicate data. We start out with a some duplicate data.
>>> 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-0-name': 'Gin and Tonic',
... 'drinks-1-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. Make sure we didn't break the valid case.
>>> 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-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Bloody Mary', ... '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 regressions import tests as regression_tests
from util import tests as util_tests from util import tests as util_tests
from widgets import tests as widgets_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 from media import media_tests
__test__ = { __test__ = {

View File

@ -292,12 +292,6 @@ checkboxes).
>>> w.value_from_datadict({}, {}, 'testing') >>> w.value_from_datadict({}, {}, 'testing')
False 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 ############################################################### # Select Widget ###############################################################
>>> w = Select() >>> w = Select()
@ -459,15 +453,6 @@ over multiple times without getting consumed:
<option value="3" selected="selected">No</option> <option value="3" selected="selected">No</option>
</select> </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]. r""" # [This concatenation is to keep the string below the jython's 32K limit].
# SelectMultiple Widget ####################################################### # 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']) >>> 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" />' 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 ######################################################### # SplitDateTimeWidget #########################################################
>>> w = SplitDateTimeWidget() >>> w = SplitDateTimeWidget()

View File

@ -15,13 +15,13 @@ class Child(models.Model):
__test__ = {'API_TESTS': """ __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 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. for the inline formset, we should get an exception.
>>> ifs = inline_formset(Parent, Child) >>> ifs = _inlineformset_factory(Parent, Child)
Traceback (most recent call last): Traceback (most recent call last):
... ...
Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> 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. These two should both work without a problem.
>>> ifs = inline_formset(Parent, Child, fk_name='mother') >>> ifs = _inlineformset_factory(Parent, Child, fk_name='mother')
>>> ifs = inline_formset(Parent, Child, fk_name='father') >>> 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 If we specify fk_name, but it isn't a ForeignKey from the child model to the
parent model, we should get an exception. 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): Traceback (most recent call last):
... ...
Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> 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 If the field specified in fk_name is not a ForeignKey, we should get an
exception. exception.
>>> ifs = inline_formset(Parent, Child, fk_name='test') >>> ifs = _inlineformset_factory(Parent, Child, fk_name='test')
Traceback (most recent call last): Traceback (most recent call last):
... ...
Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test' Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'