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:
parent
40d55238b8
commit
93c45b5704
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -1,12 +1,14 @@
|
||||
from forms import Form
|
||||
from django.utils.encoding import StrAndUnicode
|
||||
from fields import IntegerField, BooleanField
|
||||
from widgets import HiddenInput, Media
|
||||
from widgets import HiddenInput, TextInput
|
||||
from util import ErrorList, ValidationError
|
||||
|
||||
__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
|
||||
__all__ = ('BaseFormSet', 'all_valid')
|
||||
|
||||
# special field names
|
||||
FORM_COUNT_FIELD_NAME = 'COUNT'
|
||||
TOTAL_FORM_COUNT = 'TOTAL_FORMS'
|
||||
INITIAL_FORM_COUNT = 'INITIAL_FORMS'
|
||||
ORDERING_FIELD_NAME = 'ORDER'
|
||||
DELETION_FIELD_NAME = 'DELETE'
|
||||
|
||||
@ -17,13 +19,15 @@ class ManagementForm(Form):
|
||||
increment the count field of this form as well.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
|
||||
self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
|
||||
self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
|
||||
super(ManagementForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class BaseFormSet(object):
|
||||
"""A collection of instances of the same Form class."""
|
||||
|
||||
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
||||
class BaseFormSet(StrAndUnicode):
|
||||
"""
|
||||
A collection of instances of the same Form class.
|
||||
"""
|
||||
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
||||
initial=None, error_class=ErrorList):
|
||||
self.is_bound = data is not None or files is not None
|
||||
self.prefix = prefix or 'form'
|
||||
@ -32,68 +36,132 @@ class BaseFormSet(object):
|
||||
self.files = files
|
||||
self.initial = initial
|
||||
self.error_class = error_class
|
||||
self._errors = None
|
||||
self._non_form_errors = None
|
||||
# initialization is different depending on whether we recieved data, initial, or nothing
|
||||
if data or files:
|
||||
self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
|
||||
if self.management_form.is_valid():
|
||||
self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME]
|
||||
self.required_forms = self.total_forms - self.num_extra
|
||||
self.change_form_count = self.total_forms - self.num_extra
|
||||
self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
|
||||
self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
|
||||
else:
|
||||
# not sure that ValidationError is the best thing to raise here
|
||||
raise ValidationError('ManagementForm data is missing or has been tampered with')
|
||||
elif initial:
|
||||
self.change_form_count = len(initial)
|
||||
self.required_forms = len(initial)
|
||||
self.total_forms = self.required_forms + self.num_extra
|
||||
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
|
||||
self._initial_form_count = len(initial)
|
||||
self._total_form_count = self._initial_form_count + self.extra
|
||||
else:
|
||||
self.change_form_count = 0
|
||||
self.required_forms = 0
|
||||
self.total_forms = self.num_extra
|
||||
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
|
||||
self._initial_form_count = 0
|
||||
self._total_form_count = self.extra
|
||||
initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count}
|
||||
self.management_form = ManagementForm(initial=initial, auto_id=auto_id, prefix=prefix)
|
||||
|
||||
def _get_add_forms(self):
|
||||
"""Return a list of all the add forms in this ``FormSet``."""
|
||||
FormClass = self.form_class
|
||||
if not hasattr(self, '_add_forms'):
|
||||
add_forms = []
|
||||
for i in range(self.change_form_count, self.total_forms):
|
||||
kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
|
||||
if self.data:
|
||||
kwargs['data'] = self.data
|
||||
if self.files:
|
||||
kwargs['files'] = self.files
|
||||
add_form = FormClass(**kwargs)
|
||||
self.add_fields(add_form, i)
|
||||
add_forms.append(add_form)
|
||||
self._add_forms = add_forms
|
||||
return self._add_forms
|
||||
add_forms = property(_get_add_forms)
|
||||
# instantiate all the forms and put them in self.forms
|
||||
self.forms = []
|
||||
for i in range(self._total_form_count):
|
||||
self.forms.append(self._construct_form(i))
|
||||
|
||||
def _get_change_forms(self):
|
||||
"""Return a list of all the change forms in this ``FormSet``."""
|
||||
FormClass = self.form_class
|
||||
if not hasattr(self, '_change_forms'):
|
||||
change_forms = []
|
||||
for i in range(0, self.change_form_count):
|
||||
kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
|
||||
if self.data:
|
||||
kwargs['data'] = self.data
|
||||
if self.files:
|
||||
kwargs['files'] = self.files
|
||||
if self.initial:
|
||||
kwargs['initial'] = self.initial[i]
|
||||
change_form = FormClass(**kwargs)
|
||||
self.add_fields(change_form, i)
|
||||
change_forms.append(change_form)
|
||||
self._change_forms= change_forms
|
||||
return self._change_forms
|
||||
change_forms = property(_get_change_forms)
|
||||
def __unicode__(self):
|
||||
return self.as_table()
|
||||
|
||||
def _forms(self):
|
||||
return self.change_forms + self.add_forms
|
||||
forms = property(_forms)
|
||||
def _construct_form(self, i):
|
||||
"""
|
||||
Instantiates and returns the i-th form instance in a formset.
|
||||
"""
|
||||
kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
|
||||
if self.data or self.files:
|
||||
kwargs['data'] = self.data
|
||||
kwargs['files'] = self.files
|
||||
if self.initial:
|
||||
try:
|
||||
kwargs['initial'] = self.initial[i]
|
||||
except IndexError:
|
||||
pass
|
||||
# Allow extra forms to be empty.
|
||||
if i >= self._initial_form_count:
|
||||
kwargs['empty_permitted'] = True
|
||||
form = self.form(**kwargs)
|
||||
self.add_fields(form, i)
|
||||
return form
|
||||
|
||||
def _get_initial_forms(self):
|
||||
"""Return a list of all the intial forms in this formset."""
|
||||
return self.forms[:self._initial_form_count]
|
||||
initial_forms = property(_get_initial_forms)
|
||||
|
||||
def _get_extra_forms(self):
|
||||
"""Return a list of all the extra forms in this formset."""
|
||||
return self.forms[self._initial_form_count:]
|
||||
extra_forms = property(_get_extra_forms)
|
||||
|
||||
# Maybe this should just go away?
|
||||
def _get_cleaned_data(self):
|
||||
"""
|
||||
Returns a list of form.cleaned_data dicts for every form in self.forms.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
|
||||
return [form.cleaned_data for form in self.forms]
|
||||
cleaned_data = property(_get_cleaned_data)
|
||||
|
||||
def _get_deleted_forms(self):
|
||||
"""
|
||||
Returns a list of forms that have been marked for deletion. Raises an
|
||||
AttributeError is deletion is not allowed.
|
||||
"""
|
||||
if not self.is_valid() or not self.can_delete:
|
||||
raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
|
||||
# construct _deleted_form_indexes which is just a list of form indexes
|
||||
# that have had their deletion widget set to True
|
||||
if not hasattr(self, '_deleted_form_indexes'):
|
||||
self._deleted_form_indexes = []
|
||||
for i in range(0, self._total_form_count):
|
||||
form = self.forms[i]
|
||||
# if this is an extra form and hasn't changed, don't consider it
|
||||
if i >= self._initial_form_count and not form.has_changed():
|
||||
continue
|
||||
if form.cleaned_data[DELETION_FIELD_NAME]:
|
||||
self._deleted_form_indexes.append(i)
|
||||
return [self.forms[i] for i in self._deleted_form_indexes]
|
||||
deleted_forms = property(_get_deleted_forms)
|
||||
|
||||
def _get_ordered_forms(self):
|
||||
"""
|
||||
Returns a list of form in the order specified by the incoming data.
|
||||
Raises an AttributeError is deletion is not allowed.
|
||||
"""
|
||||
if not self.is_valid() or not self.can_order:
|
||||
raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
|
||||
# Construct _ordering, which is a list of (form_index, order_field_value)
|
||||
# tuples. After constructing this list, we'll sort it by order_field_value
|
||||
# so we have a way to get to the form indexes in the order specified
|
||||
# by the form data.
|
||||
if not hasattr(self, '_ordering'):
|
||||
self._ordering = []
|
||||
for i in range(0, self._total_form_count):
|
||||
form = self.forms[i]
|
||||
# if this is an extra form and hasn't changed, don't consider it
|
||||
if i >= self._initial_form_count and not form.has_changed():
|
||||
continue
|
||||
# don't add data marked for deletion to self.ordered_data
|
||||
if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
|
||||
continue
|
||||
# A sort function to order things numerically ascending, but
|
||||
# None should be sorted below anything else. Allowing None as
|
||||
# a comparison value makes it so we can leave ordering fields
|
||||
# blamk.
|
||||
def compare_ordering_values(x, y):
|
||||
if x[1] is None:
|
||||
return 1
|
||||
if y[1] is None:
|
||||
return -1
|
||||
return x[1] - y[1]
|
||||
self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
|
||||
# After we're done populating self._ordering, sort it.
|
||||
self._ordering.sort(compare_ordering_values)
|
||||
# Return a list of form.cleaned_data dicts in the order spcified by
|
||||
# the form data.
|
||||
return [self.forms[i[0]] for i in self._ordering]
|
||||
ordered_forms = property(_get_ordered_forms)
|
||||
|
||||
def non_form_errors(self):
|
||||
"""
|
||||
@ -101,63 +169,48 @@ class BaseFormSet(object):
|
||||
form -- i.e., from formset.clean(). Returns an empty ErrorList if there
|
||||
are none.
|
||||
"""
|
||||
if hasattr(self, '_non_form_errors'):
|
||||
if self._non_form_errors is not None:
|
||||
return self._non_form_errors
|
||||
return self.error_class()
|
||||
|
||||
def _get_errors(self):
|
||||
"""
|
||||
Returns a list of form.errors for every form in self.forms.
|
||||
"""
|
||||
if self._errors is None:
|
||||
self.full_clean()
|
||||
return self._errors
|
||||
errors = property(_get_errors)
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
Returns True if form.errors is empty for every form in self.forms.
|
||||
"""
|
||||
if not self.is_bound:
|
||||
return False
|
||||
# We loop over every form.errors here rather than short circuiting on the
|
||||
# first failure to make sure validation gets triggered for every form.
|
||||
forms_valid = True
|
||||
for errors in self.errors:
|
||||
if bool(errors):
|
||||
forms_valid = False
|
||||
return forms_valid and not bool(self.non_form_errors())
|
||||
|
||||
def full_clean(self):
|
||||
"""Cleans all of self.data and populates self.__errors and self.cleaned_data."""
|
||||
self._is_valid = True # Assume the formset is valid until proven otherwise.
|
||||
errors = []
|
||||
"""
|
||||
Cleans all of self.data and populates self._errors.
|
||||
"""
|
||||
self._errors = []
|
||||
if not self.is_bound: # Stop further processing.
|
||||
self.__errors = errors
|
||||
return
|
||||
self.cleaned_data = []
|
||||
self.deleted_data = []
|
||||
# Process change forms
|
||||
for form in self.change_forms:
|
||||
if form.is_valid():
|
||||
if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
|
||||
self.deleted_data.append(form.cleaned_data)
|
||||
else:
|
||||
self.cleaned_data.append(form.cleaned_data)
|
||||
else:
|
||||
self._is_valid = False
|
||||
errors.append(form.errors)
|
||||
# Process add forms in reverse so we can easily tell when the remaining
|
||||
# ones should be required.
|
||||
reamining_forms_required = False
|
||||
add_errors = []
|
||||
for i in range(len(self.add_forms)-1, -1, -1):
|
||||
form = self.add_forms[i]
|
||||
# If an add form is empty, reset it so it won't have any errors
|
||||
if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required:
|
||||
form.reset()
|
||||
continue
|
||||
else:
|
||||
reamining_forms_required = True
|
||||
if form.is_valid():
|
||||
self.cleaned_data.append(form.cleaned_data)
|
||||
else:
|
||||
self._is_valid = False
|
||||
add_errors.append(form.errors)
|
||||
add_errors.reverse()
|
||||
errors.extend(add_errors)
|
||||
# Sort cleaned_data if the formset is orderable.
|
||||
if self.orderable:
|
||||
self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
|
||||
# Give self.clean() a chance to do validation
|
||||
for i in range(0, self._total_form_count):
|
||||
form = self.forms[i]
|
||||
self._errors.append(form.errors)
|
||||
# Give self.clean() a chance to do cross-form validation.
|
||||
try:
|
||||
self.cleaned_data = self.clean()
|
||||
self.clean()
|
||||
except ValidationError, e:
|
||||
self._non_form_errors = e.messages
|
||||
self._is_valid = False
|
||||
self.errors = errors
|
||||
# If there were errors, be consistent with forms and remove the
|
||||
# cleaned_data and deleted_data attributes.
|
||||
if not self._is_valid:
|
||||
delattr(self, 'cleaned_data')
|
||||
delattr(self, 'deleted_data')
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
@ -166,36 +219,50 @@ class BaseFormSet(object):
|
||||
will not be associated with a particular form; it will be accesible
|
||||
via formset.non_form_errors()
|
||||
"""
|
||||
return self.cleaned_data
|
||||
pass
|
||||
|
||||
def add_fields(self, form, index):
|
||||
"""A hook for adding extra fields on to each form instance."""
|
||||
if self.orderable:
|
||||
form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
|
||||
if self.deletable:
|
||||
if self.can_order:
|
||||
# Only pre-fill the ordering field for initial forms.
|
||||
if index < self._initial_form_count:
|
||||
form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
|
||||
else:
|
||||
form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
|
||||
if self.can_delete:
|
||||
form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
|
||||
|
||||
def add_prefix(self, index):
|
||||
return '%s-%s' % (self.prefix, index)
|
||||
|
||||
def is_valid(self):
|
||||
if not self.is_bound:
|
||||
return False
|
||||
self.full_clean()
|
||||
return self._is_valid
|
||||
def is_multipart(self):
|
||||
"""
|
||||
Returns True if the formset needs to be multipart-encrypted, i.e. it
|
||||
has FileInput. Otherwise, False.
|
||||
"""
|
||||
return self.forms[0].is_multipart()
|
||||
|
||||
def _get_media(self):
|
||||
# All the forms on a FormSet are the same, so you only need to
|
||||
# All the forms on a FormSet are the same, so you only need to
|
||||
# interrogate the first form for media.
|
||||
if self.forms:
|
||||
return self.forms[0].media
|
||||
else:
|
||||
return Media()
|
||||
media = property(_get_media)
|
||||
|
||||
def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
|
||||
|
||||
def as_table(self):
|
||||
"Returns this formset rendered as HTML <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):
|
||||
|
@ -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
|
||||
|
@ -164,15 +164,19 @@ class Widget(object):
|
||||
of this widget. Returns None if it's not provided.
|
||||
"""
|
||||
return data.get(name, None)
|
||||
|
||||
def is_empty(self, value):
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
"""
|
||||
Given a dictionary of data and this widget's name, return True if the
|
||||
widget data is empty or False when not empty.
|
||||
Return True if data differs from initial.
|
||||
"""
|
||||
if value not in (None, ''):
|
||||
return False
|
||||
return True
|
||||
# For purposes of seeing whether something has changed, None is
|
||||
# the same as an empty string, if the data or inital value we get
|
||||
# is None, replace it w/ u''.
|
||||
data_value = data or u''
|
||||
initial_value = initial or u''
|
||||
if force_unicode(initial_value) != force_unicode(data_value):
|
||||
return True
|
||||
return False
|
||||
|
||||
def id_for_label(self, id_):
|
||||
"""
|
||||
@ -309,11 +313,11 @@ class CheckboxInput(Widget):
|
||||
# send results for unselected checkboxes.
|
||||
return False
|
||||
return super(CheckboxInput, self).value_from_datadict(data, files, name)
|
||||
|
||||
def is_empty(self, value):
|
||||
# this widget will always either be True or False, so always return the
|
||||
# opposite value so False values will make the form empty
|
||||
return not value
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
# Sometimes data or initial could be None or u'' which should be the
|
||||
# same thing as False.
|
||||
return bool(initial) != bool(data)
|
||||
|
||||
class Select(Widget):
|
||||
def __init__(self, attrs=None, choices=()):
|
||||
@ -356,12 +360,11 @@ class NullBooleanSelect(Select):
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = data.get(name, None)
|
||||
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
|
||||
|
||||
def is_empty(self, value):
|
||||
# this widget will always either be True, False or None, so always
|
||||
# return the opposite value so False and None values will make the
|
||||
# form empty.
|
||||
return not value
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
# Sometimes data or initial could be None or u'' which should be the
|
||||
# same thing as False.
|
||||
return bool(initial) != bool(data)
|
||||
|
||||
class SelectMultiple(Widget):
|
||||
def __init__(self, attrs=None, choices=()):
|
||||
@ -559,9 +562,11 @@ class MultiWidget(Widget):
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
|
||||
|
||||
def is_empty(self, value):
|
||||
for widget, val in zip(self.widgets, value):
|
||||
if not widget.is_empty(val):
|
||||
def _has_changed(self, initial, data):
|
||||
if initial is None:
|
||||
initial = [u'' for x in range(0, len(data))]
|
||||
for widget, initial, data in zip(self.widgets, initial, data):
|
||||
if not widget._has_changed(initial, data):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'
|
||||
|
||||
"""
|
||||
|
@ -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',
|
||||
... }
|
||||
|
@ -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__ = {
|
||||
|
@ -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()
|
||||
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user