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:
parent
40d55238b8
commit
93c45b5704
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -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',
|
||||||
... }
|
... }
|
||||||
|
@ -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__ = {
|
||||||
|
@ -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()
|
||||||
|
@ -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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user