Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

newforms-admin: Cleaned up the implementation and APIs of all the for…

…mset classes. Backwards-incompatible.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7270 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 93c45b570427b0a0f875a6f551d229f20e2ac5ee 1 parent 40d5523
@jkocherhans jkocherhans authored
View
31 django/contrib/admin/options.py
@@ -1,6 +1,7 @@
from django import oldforms, template
from django import newforms as forms
from django.newforms.formsets import all_valid
+from django.newforms.models import _modelform_factory, _inlineformset_factory
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets
from django.contrib.admin.util import get_deleted_objects
@@ -340,7 +341,7 @@ def form_add(self, request):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
+ return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
def form_change(self, request, obj):
"""
@@ -350,7 +351,7 @@ def form_change(self, request, obj):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)
+ return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
def save_add(self, request, model, form, formsets, post_url_continue):
"""
@@ -496,14 +497,14 @@ def add_view(self, request, form_url=''):
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
for FormSet in self.formsets_add(request):
- inline_formset = FormSet(obj, data=request.POST, files=request.FILES)
+ inline_formset = FormSet(data=request.POST, files=request.FILES, instance=obj)
inline_formsets.append(inline_formset)
if all_valid(inline_formsets) and form.is_valid():
return self.save_add(request, model, form, inline_formsets, '../%s/')
else:
form = ModelForm(initial=request.GET)
for FormSet in self.formsets_add(request):
- inline_formset = FormSet(obj)
+ inline_formset = FormSet(instance=obj)
inline_formsets.append(inline_formset)
adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
@@ -553,17 +554,17 @@ def change_view(self, request, object_id):
ModelForm = self.form_change(request, obj)
inline_formsets = []
if request.method == 'POST':
- form = ModelForm(request.POST, request.FILES)
+ form = ModelForm(request.POST, request.FILES, instance=obj)
for FormSet in self.formsets_change(request, obj):
- inline_formset = FormSet(obj, request.POST, request.FILES)
+ inline_formset = FormSet(request.POST, request.FILES, instance=obj)
inline_formsets.append(inline_formset)
if all_valid(inline_formsets) and form.is_valid():
return self.save_change(request, model, form, inline_formsets)
else:
- form = ModelForm()
+ form = ModelForm(instance=obj)
for FormSet in self.formsets_change(request, obj):
- inline_formset = FormSet(obj)
+ inline_formset = FormSet(instance=obj)
inline_formsets.append(inline_formset)
## Populate the FormWrapper.
@@ -740,7 +741,7 @@ def formset_add(self, request):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
+ return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
def formset_change(self, request, obj):
"""Returns an InlineFormSet class for use in admin change views."""
@@ -748,18 +749,18 @@ def formset_change(self, request, obj):
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
- return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
+ return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
def fieldsets_add(self, request):
if self.declared_fieldsets:
return self.declared_fieldsets
- form = self.formset_add(request).form_class
+ form = self.formset_add(request).form
return [(None, {'fields': form.base_fields.keys()})]
def fieldsets_change(self, request, obj):
if self.declared_fieldsets:
return self.declared_fieldsets
- form = self.formset_change(request, obj).form_class
+ form = self.formset_change(request, obj).form
return [(None, {'fields': form.base_fields.keys()})]
class StackedInline(InlineModelAdmin):
@@ -778,14 +779,14 @@ def __init__(self, inline, formset, fieldsets):
self.fieldsets = fieldsets
def __iter__(self):
- for form, original in zip(self.formset.change_forms, self.formset.get_queryset()):
+ for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
- for form in self.formset.add_forms:
+ for form in self.formset.extra_forms:
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
def fields(self):
for field_name in flatten_fieldsets(self.fieldsets):
- yield self.formset.form_class.base_fields[field_name]
+ yield self.formset.form.base_fields[field_name]
class InlineAdminForm(AdminForm):
"""
View
49 django/newforms/forms.py
@@ -69,7 +69,8 @@ class BaseForm(StrAndUnicode):
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
- initial=None, error_class=ErrorList, label_suffix=':'):
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@@ -78,6 +79,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
self.initial = initial or {}
self.error_class = error_class
self.label_suffix = label_suffix
+ self.empty_permitted = empty_permitted
self._errors = None # Stores the errors after clean() has been called.
# The base_fields class attribute is the *class-wide* definition of
@@ -189,24 +191,6 @@ def non_field_errors(self):
"""
return self.errors.get(NON_FIELD_ERRORS, self.error_class())
- def is_empty(self, exceptions=None):
- """
- Returns True if this form has been bound and all fields that aren't
- listed in exceptions are empty.
- """
- # TODO: This could probably use some optimization
- exceptions = exceptions or []
- for name, field in self.fields.items():
- if name in exceptions:
- continue
- # value_from_datadict() gets the data from the data dictionaries.
- # Each widget type knows how to retrieve its own data, because some
- # widgets split data over several HTML fields.
- value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
- if not field.widget.is_empty(value):
- return False
- return True
-
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
@@ -216,6 +200,10 @@ def full_clean(self):
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
+ # If the form is permitted to be empty, and none of the form data has
+ # changed from the initial data, short circuit any validation.
+ if self.empty_permitted and not self.has_changed():
+ return
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
@@ -251,11 +239,24 @@ def clean(self):
"""
return self.cleaned_data
- def reset(self):
- """Return this form to the state it was in before data was passed to it."""
- self.data = {}
- self.is_bound = False
- self.__errors = None
+ def has_changed(self):
+ """
+ Returns True if data differs from initial.
+ """
+ # XXX: For now we're asking the individual widgets whether or not the
+ # data has changed. It would probably be more efficient to hash the
+ # initial data, store it in a hidden field, and compare a hash of the
+ # submitted data, but we'd need a way to easily get the string value
+ # for a given field. Right now, that logic is embedded in the render
+ # method of each widget.
+ for name, field in self.fields.items():
+ prefixed_name = self.add_prefix(name)
+ data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
+ initial_value = self.initial.get(name, field.initial)
+ if field.widget._has_changed(initial_value, data_value):
+ #print field
+ return True
+ return False
def _get_media(self):
"""
View
313 django/newforms/formsets.py
@@ -1,12 +1,14 @@
from forms import Form
+from django.utils.encoding import StrAndUnicode
from fields import IntegerField, BooleanField
-from widgets import HiddenInput, Media
+from widgets import HiddenInput, TextInput
from util import ErrorList, ValidationError
-__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
+__all__ = ('BaseFormSet', 'all_valid')
# special field names
-FORM_COUNT_FIELD_NAME = 'COUNT'
+TOTAL_FORM_COUNT = 'TOTAL_FORMS'
+INITIAL_FORM_COUNT = 'INITIAL_FORMS'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
@@ -17,13 +19,15 @@ class ManagementForm(Form):
increment the count field of this form as well.
"""
def __init__(self, *args, **kwargs):
- self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
+ self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs)
-class BaseFormSet(object):
- """A collection of instances of the same Form class."""
-
- def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+class BaseFormSet(StrAndUnicode):
+ """
+ A collection of instances of the same Form class.
+ """
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList):
self.is_bound = data is not None or files is not None
self.prefix = prefix or 'form'
@@ -32,68 +36,132 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
self.files = files
self.initial = initial
self.error_class = error_class
+ self._errors = None
+ self._non_form_errors = None
# initialization is different depending on whether we recieved data, initial, or nothing
if data or files:
self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
if self.management_form.is_valid():
- self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME]
- self.required_forms = self.total_forms - self.num_extra
- self.change_form_count = self.total_forms - self.num_extra
+ self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
else:
- # not sure that ValidationError is the best thing to raise here
raise ValidationError('ManagementForm data is missing or has been tampered with')
elif initial:
- self.change_form_count = len(initial)
- self.required_forms = len(initial)
- self.total_forms = self.required_forms + self.num_extra
- self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
+ self._initial_form_count = len(initial)
+ self._total_form_count = self._initial_form_count + self.extra
else:
- self.change_form_count = 0
- self.required_forms = 0
- self.total_forms = self.num_extra
- self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
-
- def _get_add_forms(self):
- """Return a list of all the add forms in this ``FormSet``."""
- FormClass = self.form_class
- if not hasattr(self, '_add_forms'):
- add_forms = []
- for i in range(self.change_form_count, self.total_forms):
- kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
- if self.data:
- kwargs['data'] = self.data
- if self.files:
- kwargs['files'] = self.files
- add_form = FormClass(**kwargs)
- self.add_fields(add_form, i)
- add_forms.append(add_form)
- self._add_forms = add_forms
- return self._add_forms
- add_forms = property(_get_add_forms)
-
- def _get_change_forms(self):
- """Return a list of all the change forms in this ``FormSet``."""
- FormClass = self.form_class
- if not hasattr(self, '_change_forms'):
- change_forms = []
- for i in range(0, self.change_form_count):
- kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
- if self.data:
- kwargs['data'] = self.data
- if self.files:
- kwargs['files'] = self.files
- if self.initial:
- kwargs['initial'] = self.initial[i]
- change_form = FormClass(**kwargs)
- self.add_fields(change_form, i)
- change_forms.append(change_form)
- self._change_forms= change_forms
- return self._change_forms
- change_forms = property(_get_change_forms)
-
- def _forms(self):
- return self.change_forms + self.add_forms
- forms = property(_forms)
+ self._initial_form_count = 0
+ self._total_form_count = self.extra
+ initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count}
+ self.management_form = ManagementForm(initial=initial, auto_id=auto_id, prefix=prefix)
+
+ # instantiate all the forms and put them in self.forms
+ self.forms = []
+ for i in range(self._total_form_count):
+ self.forms.append(self._construct_form(i))
+
+ def __unicode__(self):
+ return self.as_table()
+
+ def _construct_form(self, i):
+ """
+ Instantiates and returns the i-th form instance in a formset.
+ """
+ kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+ if self.data or self.files:
+ kwargs['data'] = self.data
+ kwargs['files'] = self.files
+ if self.initial:
+ try:
+ kwargs['initial'] = self.initial[i]
+ except IndexError:
+ pass
+ # Allow extra forms to be empty.
+ if i >= self._initial_form_count:
+ kwargs['empty_permitted'] = True
+ form = self.form(**kwargs)
+ self.add_fields(form, i)
+ return form
+
+ def _get_initial_forms(self):
+ """Return a list of all the intial forms in this formset."""
+ return self.forms[:self._initial_form_count]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ """Return a list of all the extra forms in this formset."""
+ return self.forms[self._initial_form_count:]
+ extra_forms = property(_get_extra_forms)
+
+ # Maybe this should just go away?
+ def _get_cleaned_data(self):
+ """
+ Returns a list of form.cleaned_data dicts for every form in self.forms.
+ """
+ if not self.is_valid():
+ raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
+ return [form.cleaned_data for form in self.forms]
+ cleaned_data = property(_get_cleaned_data)
+
+ def _get_deleted_forms(self):
+ """
+ Returns a list of forms that have been marked for deletion. Raises an
+ AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_delete:
+ raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
+ # construct _deleted_form_indexes which is just a list of form indexes
+ # that have had their deletion widget set to True
+ if not hasattr(self, '_deleted_form_indexes'):
+ self._deleted_form_indexes = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ if form.cleaned_data[DELETION_FIELD_NAME]:
+ self._deleted_form_indexes.append(i)
+ return [self.forms[i] for i in self._deleted_form_indexes]
+ deleted_forms = property(_get_deleted_forms)
+
+ def _get_ordered_forms(self):
+ """
+ Returns a list of form in the order specified by the incoming data.
+ Raises an AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_order:
+ raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
+ # Construct _ordering, which is a list of (form_index, order_field_value)
+ # tuples. After constructing this list, we'll sort it by order_field_value
+ # so we have a way to get to the form indexes in the order specified
+ # by the form data.
+ if not hasattr(self, '_ordering'):
+ self._ordering = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ # don't add data marked for deletion to self.ordered_data
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ # A sort function to order things numerically ascending, but
+ # None should be sorted below anything else. Allowing None as
+ # a comparison value makes it so we can leave ordering fields
+ # blamk.
+ def compare_ordering_values(x, y):
+ if x[1] is None:
+ return 1
+ if y[1] is None:
+ return -1
+ return x[1] - y[1]
+ self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
+ # After we're done populating self._ordering, sort it.
+ self._ordering.sort(compare_ordering_values)
+ # Return a list of form.cleaned_data dicts in the order spcified by
+ # the form data.
+ return [self.forms[i[0]] for i in self._ordering]
+ ordered_forms = property(_get_ordered_forms)
def non_form_errors(self):
"""
@@ -101,63 +169,48 @@ def non_form_errors(self):
form -- i.e., from formset.clean(). Returns an empty ErrorList if there
are none.
"""
- if hasattr(self, '_non_form_errors'):
+ if self._non_form_errors is not None:
return self._non_form_errors
return self.error_class()
+ def _get_errors(self):
+ """
+ Returns a list of form.errors for every form in self.forms.
+ """
+ if self._errors is None:
+ self.full_clean()
+ return self._errors
+ errors = property(_get_errors)
+
+ def is_valid(self):
+ """
+ Returns True if form.errors is empty for every form in self.forms.
+ """
+ if not self.is_bound:
+ return False
+ # We loop over every form.errors here rather than short circuiting on the
+ # first failure to make sure validation gets triggered for every form.
+ forms_valid = True
+ for errors in self.errors:
+ if bool(errors):
+ forms_valid = False
+ return forms_valid and not bool(self.non_form_errors())
+
def full_clean(self):
- """Cleans all of self.data and populates self.__errors and self.cleaned_data."""
- self._is_valid = True # Assume the formset is valid until proven otherwise.
- errors = []
+ """
+ Cleans all of self.data and populates self._errors.
+ """
+ self._errors = []
if not self.is_bound: # Stop further processing.
- self.__errors = errors
return
- self.cleaned_data = []
- self.deleted_data = []
- # Process change forms
- for form in self.change_forms:
- if form.is_valid():
- if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
- self.deleted_data.append(form.cleaned_data)
- else:
- self.cleaned_data.append(form.cleaned_data)
- else:
- self._is_valid = False
- errors.append(form.errors)
- # Process add forms in reverse so we can easily tell when the remaining
- # ones should be required.
- reamining_forms_required = False
- add_errors = []
- for i in range(len(self.add_forms)-1, -1, -1):
- form = self.add_forms[i]
- # If an add form is empty, reset it so it won't have any errors
- if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required:
- form.reset()
- continue
- else:
- reamining_forms_required = True
- if form.is_valid():
- self.cleaned_data.append(form.cleaned_data)
- else:
- self._is_valid = False
- add_errors.append(form.errors)
- add_errors.reverse()
- errors.extend(add_errors)
- # Sort cleaned_data if the formset is orderable.
- if self.orderable:
- self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
- # Give self.clean() a chance to do validation
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ self._errors.append(form.errors)
+ # Give self.clean() a chance to do cross-form validation.
try:
- self.cleaned_data = self.clean()
+ self.clean()
except ValidationError, e:
self._non_form_errors = e.messages
- self._is_valid = False
- self.errors = errors
- # If there were errors, be consistent with forms and remove the
- # cleaned_data and deleted_data attributes.
- if not self._is_valid:
- delattr(self, 'cleaned_data')
- delattr(self, 'deleted_data')
def clean(self):
"""
@@ -166,36 +219,50 @@ def clean(self):
will not be associated with a particular form; it will be accesible
via formset.non_form_errors()
"""
- return self.cleaned_data
+ pass
def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance."""
- if self.orderable:
- form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
- if self.deletable:
+ if self.can_order:
+ # Only pre-fill the ordering field for initial forms.
+ if index < self._initial_form_count:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
+ else:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
+ if self.can_delete:
form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
def add_prefix(self, index):
return '%s-%s' % (self.prefix, index)
- def is_valid(self):
- if not self.is_bound:
- return False
- self.full_clean()
- return self._is_valid
+ def is_multipart(self):
+ """
+ Returns True if the formset needs to be multipart-encrypted, i.e. it
+ has FileInput. Otherwise, False.
+ """
+ return self.forms[0].is_multipart()
def _get_media(self):
- # All the forms on a FormSet are the same, so you only need to
+ # All the forms on a FormSet are the same, so you only need to
# interrogate the first form for media.
if self.forms:
return self.forms[0].media
else:
return Media()
media = property(_get_media)
-
-def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
+
+ def as_table(self):
+ "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
+ # XXX: there is no semantic division between forms here, there
+ # probably should be. It might make sense to render each form as a
+ # table row with each field as a td.
+ forms = u' '.join([form.as_table() for form in self.forms])
+ return u'\n'.join([unicode(self.management_form), forms])
+
+# XXX: This API *will* change. Use at your own risk.
+def _formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False):
"""Return a FormSet for the given form class."""
- attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
+ attrs = {'form': form, 'extra': extra, 'can_order': can_order, 'can_delete': can_delete}
return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets):
View
361 django/newforms/models.py
@@ -13,13 +13,12 @@
from util import ValidationError, ErrorList
from forms import BaseForm, get_declared_fields
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
-from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
+from formsets import BaseFormSet, _formset_factory, DELETION_FIELD_NAME
__all__ = (
'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
- 'formset_for_model', 'inline_formset',
'ModelChoiceField', 'ModelMultipleChoiceField',
)
@@ -245,7 +244,7 @@ def __new__(cls, name, bases, attrs,
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':',
- instance=None):
+ empty_permitted=False, instance=None):
opts = self._meta
if instance is None:
# if we didn't get an instance, instantiate a new one
@@ -257,7 +256,8 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
# if initial was provided, it should override the values from instance
if initial is not None:
object_data.update(initial)
- BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix)
+ BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
+ error_class, label_suffix, empty_permitted)
def save(self, commit=True):
"""
@@ -276,163 +276,45 @@ def save(self, commit=True):
class ModelForm(BaseModelForm):
__metaclass__ = ModelFormMetaclass
+# XXX: This API *will* change. Use at your own risk.
+def _modelform_factory(model, form=BaseForm, fields=None, exclude=None,
+ formfield_callback=lambda f: f.formfield()):
+ # HACK: we should be able to construct a ModelForm without creating
+ # and passing in a temporary inner class
+ class Meta:
+ pass
+ setattr(Meta, 'model', model)
+ setattr(Meta, 'fields', fields)
+ setattr(Meta, 'exclude', exclude)
+ class_name = model.__name__ + 'Form'
+ return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta},
+ formfield_callback=formfield_callback)
-# Fields #####################################################################
-
-class QuerySetIterator(object):
- def __init__(self, queryset, empty_label, cache_choices):
- self.queryset = queryset
- self.empty_label = empty_label
- self.cache_choices = cache_choices
-
- def __iter__(self):
- if self.empty_label is not None:
- yield (u"", self.empty_label)
- for obj in self.queryset:
- yield (obj.pk, smart_unicode(obj))
- # Clear the QuerySet cache if required.
- if not self.cache_choices:
- self.queryset._result_cache = None
-
-class ModelChoiceField(ChoiceField):
- """A ChoiceField whose choices are a model QuerySet."""
- # This class is a subclass of ChoiceField for purity, but it doesn't
- # actually use any of ChoiceField's implementation.
- default_error_messages = {
- 'invalid_choice': _(u'Select a valid choice. That choice is not one of'
- u' the available choices.'),
- }
-
- def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
- required=True, widget=Select, label=None, initial=None,
- help_text=None, *args, **kwargs):
- self.empty_label = empty_label
- self.cache_choices = cache_choices
- # Call Field instead of ChoiceField __init__() because we don't need
- # ChoiceField.__init__().
- Field.__init__(self, required, widget, label, initial, help_text,
- *args, **kwargs)
- self.queryset = queryset
-
- def _get_queryset(self):
- return self._queryset
-
- def _set_queryset(self, queryset):
- self._queryset = queryset
- self.widget.choices = self.choices
-
- queryset = property(_get_queryset, _set_queryset)
-
- def _get_choices(self):
- # If self._choices is set, then somebody must have manually set
- # the property self.choices. In this case, just return self._choices.
- if hasattr(self, '_choices'):
- return self._choices
- # Otherwise, execute the QuerySet in self.queryset to determine the
- # choices dynamically. Return a fresh QuerySetIterator that has not
- # been consumed. Note that we're instantiating a new QuerySetIterator
- # *each* time _get_choices() is called (and, thus, each time
- # self.choices is accessed) so that we can ensure the QuerySet has not
- # been consumed.
- return QuerySetIterator(self.queryset, self.empty_label,
- self.cache_choices)
-
- def _set_choices(self, value):
- # This method is copied from ChoiceField._set_choices(). It's necessary
- # because property() doesn't allow a subclass to overwrite only
- # _get_choices without implementing _set_choices.
- self._choices = self.widget.choices = list(value)
-
- choices = property(_get_choices, _set_choices)
-
- def clean(self, value):
- Field.clean(self, value)
- if value in EMPTY_VALUES:
- return None
- try:
- value = self.queryset.get(pk=value)
- except self.queryset.model.DoesNotExist:
- raise ValidationError(self.error_messages['invalid_choice'])
- return value
-
-class ModelMultipleChoiceField(ModelChoiceField):
- """A MultipleChoiceField whose choices are a model QuerySet."""
- hidden_widget = MultipleHiddenInput
- default_error_messages = {
- 'list': _(u'Enter a list of values.'),
- 'invalid_choice': _(u'Select a valid choice. %s is not one of the'
- u' available choices.'),
- }
-
- def __init__(self, queryset, cache_choices=False, required=True,
- widget=SelectMultiple, label=None, initial=None,
- help_text=None, *args, **kwargs):
- super(ModelMultipleChoiceField, self).__init__(queryset, None,
- cache_choices, required, widget, label, initial, help_text,
- *args, **kwargs)
-
- def clean(self, value):
- if self.required and not value:
- raise ValidationError(self.error_messages['required'])
- elif not self.required and not value:
- return []
- if not isinstance(value, (list, tuple)):
- raise ValidationError(self.error_messages['list'])
- final_values = []
- for val in value:
- try:
- obj = self.queryset.get(pk=val)
- except self.queryset.model.DoesNotExist:
- raise ValidationError(self.error_messages['invalid_choice'] % val)
- else:
- final_values.append(obj)
- return final_values
-
-# Model-FormSet integration ###################################################
-
-def initial_data(instance, fields=None):
- """
- Return a dictionary from data in ``instance`` that is suitable for
- use as a ``Form`` constructor's ``initial`` argument.
- Provide ``fields`` to specify the names of specific fields to return.
- All field values in the instance will be returned if ``fields`` is not
- provided.
- """
- # avoid a circular import
- from django.db.models.fields.related import ManyToManyField
- opts = instance._meta
- initial = {}
- for f in opts.fields + opts.many_to_many:
- if not f.editable:
- continue
- if fields and not f.name in fields:
- continue
- if isinstance(f, ManyToManyField):
- # MultipleChoiceWidget needs a list of ints, not object instances.
- initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
- else:
- initial[f.name] = f.value_from_object(instance)
- return initial
+# ModelFormSets ##############################################################
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):
+ 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}
- self.queryset = qs
- kwargs['initial'] = [initial_data(obj) for obj in qs]
+ 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_instance(self, form, instance, commit=True):
+ 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)
@@ -443,30 +325,30 @@ def save(self, commit=True):
return self.save_existing_objects(commit) + self.save_new_objects(commit)
def save_existing_objects(self, commit=True):
- if not self.queryset:
+ 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.queryset:
+ for obj in self.get_queryset():
existing_objects[obj.pk] = obj
saved_instances = []
- for form in self.change_forms:
+ for form in self.initial_forms:
obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
- if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
obj.delete()
else:
- saved_instances.append(self.save_instance(form, obj, commit=commit))
+ 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.add_forms:
- if form.is_empty():
+ 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.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
continue
new_objects.append(self.save_new(form, commit=commit))
return new_objects
@@ -477,30 +359,31 @@ def add_fields(self, form, index):
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):
+# 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. 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.
+ Returns a FormSet class for the given Django model class.
"""
- form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
- FormSet = formset_for_form(form, formset, extra, orderable, deletable)
+ 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
-class InlineFormset(BaseModelFormSet):
+
+# InlineFormSets #############################################################
+
+class BaseInlineFormset(BaseModelFormSet):
"""A formset for child objects related to a parent."""
- def __init__(self, instance, data=None, files=None):
+ 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()
- qs = self.get_queryset()
- super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)
+ super(BaseInlineFormset, self).__init__(data, files, prefix=self.rel_name)
def get_queryset(self):
"""
@@ -515,7 +398,7 @@ def save_new(self, form, commit=True):
new_obj = self.model(**kwargs)
return save_instance(form, new_obj, commit=commit)
-def get_foreign_key(parent_model, model, fk_name=None):
+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.
@@ -542,25 +425,141 @@ def get_foreign_key(parent_model, model, fk_name=None):
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()):
+
+# 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)
+ 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
+
+ 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 #####################################################################
+
+class QuerySetIterator(object):
+ def __init__(self, queryset, empty_label, cache_choices):
+ self.queryset = queryset
+ self.empty_label = empty_label
+ self.cache_choices = cache_choices
+
+ def __iter__(self):
+ if self.empty_label is not None:
+ yield (u"", self.empty_label)
+ for obj in self.queryset:
+ yield (obj.pk, smart_unicode(obj))
+ # Clear the QuerySet cache if required.
+ if not self.cache_choices:
+ self.queryset._result_cache = None
+
+class ModelChoiceField(ChoiceField):
+ """A ChoiceField whose choices are a model QuerySet."""
+ # This class is a subclass of ChoiceField for purity, but it doesn't
+ # actually use any of ChoiceField's implementation.
+ default_error_messages = {
+ 'invalid_choice': _(u'Select a valid choice. That choice is not one of'
+ u' the available choices.'),
+ }
+
+ def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
+ required=True, widget=Select, label=None, initial=None,
+ help_text=None, *args, **kwargs):
+ self.empty_label = empty_label
+ self.cache_choices = cache_choices
+ # Call Field instead of ChoiceField __init__() because we don't need
+ # ChoiceField.__init__().
+ Field.__init__(self, required, widget, label, initial, help_text,
+ *args, **kwargs)
+ self.queryset = queryset
+
+ def _get_queryset(self):
+ return self._queryset
+
+ def _set_queryset(self, queryset):
+ self._queryset = queryset
+ self.widget.choices = self.choices
+
+ queryset = property(_get_queryset, _set_queryset)
+
+ def _get_choices(self):
+ # If self._choices is set, then somebody must have manually set
+ # the property self.choices. In this case, just return self._choices.
+ if hasattr(self, '_choices'):
+ return self._choices
+ # Otherwise, execute the QuerySet in self.queryset to determine the
+ # choices dynamically. Return a fresh QuerySetIterator that has not
+ # been consumed. Note that we're instantiating a new QuerySetIterator
+ # *each* time _get_choices() is called (and, thus, each time
+ # self.choices is accessed) so that we can ensure the QuerySet has not
+ # been consumed.
+ return QuerySetIterator(self.queryset, self.empty_label,
+ self.cache_choices)
+
+ def _set_choices(self, value):
+ # This method is copied from ChoiceField._set_choices(). It's necessary
+ # because property() doesn't allow a subclass to overwrite only
+ # _get_choices without implementing _set_choices.
+ self._choices = self.widget.choices = list(value)
+
+ choices = property(_get_choices, _set_choices)
+
+ def clean(self, value):
+ Field.clean(self, value)
+ if value in EMPTY_VALUES:
+ return None
+ try:
+ value = self.queryset.get(pk=value)
+ except self.queryset.model.DoesNotExist:
+ raise ValidationError(self.error_messages['invalid_choice'])
+ return value
+
+class ModelMultipleChoiceField(ModelChoiceField):
+ """A MultipleChoiceField whose choices are a model QuerySet."""
+ hidden_widget = MultipleHiddenInput
+ default_error_messages = {
+ 'list': _(u'Enter a list of values.'),
+ 'invalid_choice': _(u'Select a valid choice. %s is not one of the'
+ u' available choices.'),
+ }
+
+ def __init__(self, queryset, cache_choices=False, required=True,
+ widget=SelectMultiple, label=None, initial=None,
+ help_text=None, *args, **kwargs):
+ super(ModelMultipleChoiceField, self).__init__(queryset, None,
+ cache_choices, required, widget, label, initial, help_text,
+ *args, **kwargs)
+
+ def clean(self, value):
+ if self.required and not value:
+ raise ValidationError(self.error_messages['required'])
+ elif not self.required and not value:
+ return []
+ if not isinstance(value, (list, tuple)):
+ raise ValidationError(self.error_messages['list'])
+ final_values = []
+ for val in value:
+ try:
+ obj = self.queryset.get(pk=val)
+ except self.queryset.model.DoesNotExist:
+ raise ValidationError(self.error_messages['invalid_choice'] % val)
+ else:
+ final_values.append(obj)
+ return final_values
View
47 django/newforms/widgets.py
@@ -164,15 +164,19 @@ def value_from_datadict(self, data, files, name):
of this widget. Returns None if it's not provided.
"""
return data.get(name, None)
-
- def is_empty(self, value):
+
+ def _has_changed(self, initial, data):
"""
- Given a dictionary of data and this widget's name, return True if the
- widget data is empty or False when not empty.
+ Return True if data differs from initial.
"""
- if value not in (None, ''):
- return False
- return True
+ # For purposes of seeing whether something has changed, None is
+ # the same as an empty string, if the data or inital value we get
+ # is None, replace it w/ u''.
+ data_value = data or u''
+ initial_value = initial or u''
+ if force_unicode(initial_value) != force_unicode(data_value):
+ return True
+ return False
def id_for_label(self, id_):
"""
@@ -309,11 +313,11 @@ def value_from_datadict(self, data, files, name):
# send results for unselected checkboxes.
return False
return super(CheckboxInput, self).value_from_datadict(data, files, name)
-
- def is_empty(self, value):
- # this widget will always either be True or False, so always return the
- # opposite value so False values will make the form empty
- return not value
+
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
class Select(Widget):
def __init__(self, attrs=None, choices=()):
@@ -356,12 +360,11 @@ def render(self, name, value, attrs=None, choices=()):
def value_from_datadict(self, data, files, name):
value = data.get(name, None)
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
-
- def is_empty(self, value):
- # this widget will always either be True, False or None, so always
- # return the opposite value so False and None values will make the
- # form empty.
- return not value
+
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
class SelectMultiple(Widget):
def __init__(self, attrs=None, choices=()):
@@ -559,9 +562,11 @@ def id_for_label(self, id_):
def value_from_datadict(self, data, files, name):
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
- def is_empty(self, value):
- for widget, val in zip(self.widgets, value):
- if not widget.is_empty(val):
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = [u'' for x in range(0, len(data))]
+ for widget, initial, data in zip(self.widgets, initial, data):
+ if not widget._has_changed(initial, data):
return False
return True
View
51 tests/modeltests/model_formsets/models.py
@@ -16,12 +16,12 @@ def __unicode__(self):
__test__ = {'API_TESTS': """
->>> from django.newforms.models import formset_for_model
+>>> from django.newforms.models import _modelformset_factory
>>> qs = Author.objects.all()
->>> AuthorFormSet = formset_for_model(Author, extra=3)
+>>> AuthorFormSet = _modelformset_factory(Author, extra=3)
->>> formset = AuthorFormSet(qs)
+>>> formset = AuthorFormSet(queryset=qs)
>>> for form in formset.forms:
... print form.as_p()
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
@@ -29,13 +29,14 @@ def __unicode__(self):
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
>>> data = {
-... 'form-COUNT': '3',
+... 'form-TOTAL_FORMS': '3', # the number of forms rendered
+... 'form-INITIAL_FORMS': '0', # the number of forms with initial data
... 'form-0-name': 'Charles Baudelaire',
... 'form-1-name': 'Arthur Rimbaud',
... 'form-2-name': '',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
@@ -54,9 +55,9 @@ def __unicode__(self):
but in that case we'll use it to display them in alphabetical order by name.
>>> qs = Author.objects.order_by('name')
->>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False)
+>>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=False)
->>> formset = AuthorFormSet(qs)
+>>> formset = AuthorFormSet(queryset=qs)
>>> for form in formset.forms:
... print form.as_p()
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
@@ -65,7 +66,8 @@ def __unicode__(self):
>>> data = {
-... 'form-COUNT': '3',
+... 'form-TOTAL_FORMS': '3', # the number of forms rendered
+... 'form-INITIAL_FORMS': '2', # the number of forms with initial data
... 'form-0-id': '2',
... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1',
@@ -73,7 +75,7 @@ def __unicode__(self):
... 'form-2-name': 'Paul Verlaine',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
@@ -91,9 +93,9 @@ def __unicode__(self):
deltetion, make sure we don't save that form.
>>> qs = Author.objects.order_by('name')
->>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
+>>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=True)
->>> formset = AuthorFormSet(qs)
+>>> formset = AuthorFormSet(queryset=qs)
>>> for form in formset.forms:
... print form.as_p()
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>
@@ -106,7 +108,8 @@ def __unicode__(self):
<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></p>
>>> data = {
-... 'form-COUNT': '4',
+... 'form-TOTAL_FORMS': '4', # the number of forms rendered
+... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
... 'form-0-id': '2',
... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1',
@@ -117,7 +120,7 @@ def __unicode__(self):
... 'form-3-DELETE': 'on',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
@@ -131,15 +134,17 @@ def __unicode__(self):
Paul Verlaine
+# Inline Formsets ############################################################
+
We can also create a formset that is tied to a parent model. This is how the
admin system's edit inline functionality works.
->>> from django.newforms.models import inline_formset
+>>> from django.newforms.models import _inlineformset_factory
->>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
+>>> AuthorBooksFormSet = _inlineformset_factory(Author, Book, can_delete=False, extra=3)
>>> author = Author.objects.get(name='Charles Baudelaire')
->>> formset = AuthorBooksFormSet(author)
+>>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms:
... print form.as_p()
<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
@@ -147,13 +152,14 @@ def __unicode__(self):
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
>>> data = {
-... 'book_set-COUNT': '3',
+... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
+... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': '',
... 'book_set-2-title': '',
... }
->>> formset = AuthorBooksFormSet(author, data=data)
+>>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid()
True
@@ -169,10 +175,10 @@ def __unicode__(self):
one. This time though, an edit form will be available for every existing
book.
->>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
+>>> AuthorBooksFormSet = _inlineformset_factory(Author, Book, can_delete=False, extra=2)
>>> author = Author.objects.get(name='Charles Baudelaire')
->>> formset = AuthorBooksFormSet(author)
+>>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms:
... print form.as_p()
<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
@@ -180,14 +186,15 @@ def __unicode__(self):
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
>>> data = {
-... 'book_set-COUNT': '3',
+... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
+... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data
... 'book_set-0-id': '1',
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': 'Le Spleen de Paris',
... 'book_set-2-title': '',
... }
->>> formset = AuthorBooksFormSet(author, data=data)
+>>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid()
True
View
51 tests/regressiontests/forms/forms.py
@@ -1603,4 +1603,55 @@
<p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p>
<input type="submit" />
</form>
+
+
+# The empty_permitted attribute ##############################################
+
+Sometimes (pretty much in formsets) we want to allow a form to pass validation
+if it is completely empty. We can accomplish this by using the empty_permitted
+agrument to a form constructor.
+
+>>> class SongForm(Form):
+... artist = CharField()
+... name = CharField()
+
+First let's show what happens id empty_permitted=False (the default):
+
+>>> data = {'artist': '', 'song': ''}
+
+>>> form = SongForm(data, empty_permitted=False)
+>>> form.is_valid()
+False
+>>> form.errors
+{'name': [u'This field is required.'], 'artist': [u'This field is required.']}
+>>> form.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'SongForm' object has no attribute 'cleaned_data'
+
+
+Now let's show what happens when empty_permitted=True and the form is empty.
+
+>>> form = SongForm(data, empty_permitted=True)
+>>> form.is_valid()
+True
+>>> form.errors
+{}
+>>> form.cleaned_data
+{}
+
+But if we fill in data for one of the fields, the form is no longer empty and
+the whole thing must pass validation.
+
+>>> data = {'artist': 'The Doors', 'song': ''}
+>>> form = SongForm(data, empty_permitted=False)
+>>> form.is_valid()
+False
+>>> form.errors
+{'name': [u'This field is required.']}
+>>> form.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'SongForm' object has no attribute 'cleaned_data'
+
"""
View
191 tests/regressiontests/forms/formsets.py
@@ -1,38 +1,39 @@
# -*- coding: utf-8 -*-
-formset_tests = """
+tests = """
# Basic FormSet creation and usage ############################################
FormSet allows us to use multiple instance of the same form on 1 page. For now,
-the best way to create a FormSet is by using the formset_for_form function.
+the best way to create a FormSet is by using the _formset_factory function.
>>> from django.newforms import Form, CharField, IntegerField, ValidationError
->>> from django.newforms.formsets import formset_for_form, BaseFormSet
+>>> from django.newforms.formsets import _formset_factory, BaseFormSet
>>> class Choice(Form):
... choice = CharField()
... votes = IntegerField()
->>> ChoiceFormSet = formset_for_form(Choice)
-
+>>> ChoiceFormSet = _formset_factory(Choice)
A FormSet constructor takes the same arguments as Form. Let's create a FormSet
for adding data. By default, it displays 1 blank form. It can display more,
but we'll look at how to do so later.
>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
->>> for form in formset.forms:
-... print form.as_ul()
-<li>Choice: <input type="text" name="choices-0-choice" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" /></li>
+>>> print formset
+<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" />
+<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
+<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>
+
On thing to note is that there needs to be a special value in the data. This
value tells the FormSet how many forms were displayed so it can tell how
many forms it needs to clean and validate. You could use javascript to create
new forms on the client side, but they won't get validated unless you increment
-the COUNT field appropriately.
+the TOTAL_FORMS field appropriately.
>>> data = {
-... 'choices-COUNT': '1', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... }
@@ -45,7 +46,7 @@
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
+>>> [form.cleaned_data for form in formset.forms]
[{'votes': 100, 'choice': u'Calexico'}]
If a FormSet was not passed any data, its is_valid method should return False.
@@ -57,7 +58,8 @@
any of the forms.
>>> data = {
-... 'choices-COUNT': '1', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '',
... }
@@ -68,13 +70,6 @@
>>> formset.errors
[{'votes': [u'This field is required.']}]
-Like a Form instance, cleaned_data won't exist if the formset wasn't validated.
-
->>> formset.cleaned_data
-Traceback (most recent call last):
-...
-AttributeError: 'ChoiceFormSet' object has no attribute 'cleaned_data'
-
We can also prefill a FormSet with existing data by providing an ``initial``
argument to the constructor. ``initial`` should be a list of dicts. By default,
@@ -93,7 +88,8 @@
Let's simulate what would happen if we submitted this form.
>>> data = {
-... 'choices-COUNT': '2', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': '',
@@ -103,8 +99,8 @@
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[{'votes': 100, 'choice': u'Calexico'}]
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'choice': u'Calexico'}, {}]
But the second form was blank! Shouldn't we get some errors? No. If we display
a form as blank, it's ok for it to be submitted as blank. If we fill out even
@@ -113,7 +109,8 @@
handle that later.
>>> data = {
-... 'choices-COUNT': '2', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists',
@@ -126,13 +123,13 @@
>>> formset.errors
[{}, {'votes': [u'This field is required.']}]
-
If we delete data that was pre-filled, we should get an error. Simply removing
data from form fields isn't the proper way to delete it. We'll see how to
handle that case later.
>>> data = {
-... 'choices-COUNT': '2', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-0-choice': '', # deleted value
... 'choices-0-votes': '', # deleted value
... 'choices-1-choice': '',
@@ -143,15 +140,15 @@
>>> formset.is_valid()
False
>>> formset.errors
-[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}]
+[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]
# Displaying more than 1 blank form ###########################################
We can also display more than 1 empty form at a time. To do so, pass a
-num_extra argument to formset_for_form.
+extra argument to _formset_factory.
->>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)
+>>> ChoiceFormSet = _formset_factory(Choice, extra=3)
>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms:
@@ -168,7 +165,8 @@
number of forms to be completed.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': '',
... 'choices-0-votes': '',
... 'choices-1-choice': '',
@@ -180,14 +178,15 @@
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[]
+>>> [form.cleaned_data for form in formset.forms]
+[{}, {}, {}]
We can just fill out one of the forms.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': '',
@@ -199,14 +198,15 @@
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[{'votes': 100, 'choice': u'Calexico'}]
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'choice': u'Calexico'}, {}, {}]
And once again, if we try to partially complete a form, validation will fail.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists',
@@ -219,10 +219,10 @@
>>> formset.is_valid()
False
>>> formset.errors
-[{}, {'votes': [u'This field is required.']}]
+[{}, {'votes': [u'This field is required.']}, {}]
-The num_extra argument also works when the formset is pre-filled with initial
+The extra argument also works when the formset is pre-filled with initial
data.
>>> initial = [{'choice': u'Calexico', 'votes': 100}]
@@ -239,36 +239,13 @@
<li>Votes: <input type="text" name="choices-3-votes" /></li>
-If we try to skip a form, even if it was initially displayed as blank, we will
-get an error.
-
->>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
-... 'choices-0-choice': 'Calexico',
-... 'choices-0-votes': '100',
-... 'choices-1-choice': '',
-... 'choices-1-votes': '',
-... 'choices-2-choice': 'The Decemberists',
-... 'choices-2-votes': '12',
-... 'choices-3-choice': '',
-... 'choices-3-votes': '',
-... }
-
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
->>> formset.is_valid()
-False
->>> formset.errors
-[{}, {'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]
-
-
# FormSets with deletion ######################################################
We can easily add deletion ability to a FormSet with an agrument to
-formset_for_form. This will add a boolean field to each form instance. When
-that boolean field is True, the cleaned data will be in formset.deleted_data
-rather than formset.cleaned_data
+_formset_factory. This will add a boolean field to each form instance. When
+that boolean field is True, the form will be in formset.deleted_forms
->>> ChoiceFormSet = formset_for_form(Choice, deletable=True)
+>>> ChoiceFormSet = _formset_factory(Choice, can_delete=True)
>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
@@ -288,7 +265,8 @@
'on'. Let's go ahead and delete Fergie.
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-DELETE': '',
@@ -303,22 +281,23 @@
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> formset.cleaned_data
-[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}]
->>> formset.deleted_data
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}]
+>>> [form.cleaned_data for form in formset.deleted_forms]
[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]
+
# FormSets with ordering ######################################################
We can also add ordering ability to a FormSet with an agrument to
-formset_for_form. This will add a integer field to each form instance. When
-form validation succeeds, formset.cleaned_data will have the data in the correct
+_formset_factory. This will add a integer field to each form instance. When
+form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct
order specified by the ordering fields. If a number is duplicated in the set
of ordering fields, for instance form 0 and form 3 are both marked as 1, then
the form index used as a secondary ordering criteria. In order to put
something at the front of the list, you'd need to set it's order to 0.
->>> ChoiceFormSet = formset_for_form(Choice, orderable=True)
+>>> ChoiceFormSet = _formset_factory(Choice, can_order=True)
>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
@@ -332,10 +311,11 @@
<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
-<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
+<li>Order: <input type="text" name="choices-2-ORDER" /></li>
>>> data = {
-... 'choices-COUNT': '3', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@@ -350,17 +330,48 @@
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> for cleaned_data in formset.cleaned_data:
-... print cleaned_data
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}
+Ordering fields are allowed to be left blank, and if they *are* left blank,
+they will be sorted below everything else.
+
+>>> data = {
+... 'choices-TOTAL_FORMS': '4', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
+... 'choices-0-choice': 'Calexico',
+... 'choices-0-votes': '100',
+... 'choices-0-ORDER': '1',
+... 'choices-1-choice': 'Fergie',
+... 'choices-1-votes': '900',
+... 'choices-1-ORDER': '2',
+... 'choices-2-choice': 'The Decemberists',
+... 'choices-2-votes': '500',
+... 'choices-2-ORDER': '',
+... 'choices-3-choice': 'Basia Bulat',
+... 'choices-3-votes': '50',
+... 'choices-3-ORDER': '',
+... }
+
+>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset.is_valid()
+True
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
+{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
+{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}
+{'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'}
+{'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'}
+
+
# FormSets with ordering + deletion ###########################################
Let's try throwing ordering and deletion into the same form.
->>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)
+>>> ChoiceFormSet = _formset_factory(Choice, can_order=True, can_delete=True)
>>> initial = [
... {'choice': u'Calexico', 'votes': 100},
@@ -384,13 +395,14 @@
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></li>
-<li>Order: <input type="text" name="choices-3-ORDER" value="4" /></li>
+<li>Order: <input type="text" name="choices-3-ORDER" /></li>
<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>
Let's delete Fergie, and put The Decemberists ahead of Calexico.
>>> data = {
-... 'choices-COUNT': '4', # the number of forms rendered
+... 'choices-TOTAL_FORMS': '4', # the number of forms rendered
+... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@@ -405,18 +417,18 @@
... 'choices-2-DELETE': '',
... 'choices-3-choice': '',
... 'choices-3-votes': '',
-... 'choices-3-ORDER': '4',
+... 'choices-3-ORDER': '',
... 'choices-3-DELETE': '',
... }
>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
->>> for cleaned_data in formset.cleaned_data:
-... print cleaned_data
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'}
->>> formset.deleted_data
+>>> [form.cleaned_data for form in formset.deleted_forms]
[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]
@@ -433,10 +445,10 @@
...
>>> class FavoriteDrinksFormSet(BaseFormSet):
-... form_class = FavoriteDrinkForm
-... num_extra = 2
-... orderable = False
-... deletable = False
+... form = FavoriteDrinkForm
+... extra = 2
+... can_order = False
+... can_delete = False
...
... def clean(self):
... seen_drinks = []
@@ -444,13 +456,13 @@
... if drink['name'] in seen_drinks:
... raise ValidationError('You may only specify a drink once.')
... seen_drinks.append(drink['name'])
-... return self.cleaned_data
...
We start out with a some duplicate data.
>>> data = {
-... 'drinks-COUNT': '2',
+... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
+... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
... 'drinks-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Gin and Tonic',
... }
@@ -470,7 +482,8 @@
Make sure we didn't break the valid case.
>>> data = {
-... 'drinks-COUNT': '2',
+... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
+... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
... 'drinks-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Bloody Mary',
... }
View
2  tests/regressiontests/forms/tests.py
@@ -26,7 +26,7 @@
from regressions import tests as regression_tests
from util import tests as util_tests
from widgets import tests as widgets_tests
-from formsets import formset_tests
+from formsets import tests as formset_tests
from media import media_tests
__test__ = {
View
25 tests/regressiontests/forms/widgets.py
@@ -292,12 +292,6 @@
>>> w.value_from_datadict({}, {}, 'testing')
False
-The CheckboxInput widget will always be empty when there is a False value
->>> w.is_empty(False)
-True
->>> w.is_empty(True)
-False
-
# Select Widget ###############################################################
>>> w = Select()
@@ -459,15 +453,6 @@
<option value="3" selected="selected">No</option>
</select>
-The NullBooleanSelect widget will always be empty when Unknown or No is selected
-as its value. This is to stay compliant with the CheckboxInput behavior
->>> w.is_empty(False)
-True
->>> w.is_empty(None)
-True
->>> w.is_empty(True)
-False
-
""" + \
r""" # [This concatenation is to keep the string below the jython's 32K limit].
# SelectMultiple Widget #######################################################
@@ -910,16 +895,6 @@
>>> w.render('name', ['john', 'lennon'])
u'<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />'
-The MultiWidget will be empty only when all widgets are considered empty.
->>> w.is_empty(['john', 'lennon'])
-False
->>> w.is_empty(['john', ''])
-False
->>> w.is_empty(['', ''])
-True
->>> w.is_empty([None, None])
-True
-
# SplitDateTimeWidget #########################################################
>>> w = SplitDateTimeWidget()
View
12 tests/regressiontests/inline_formsets/models.py
@@ -15,13 +15,13 @@ class Child(models.Model):
__test__ = {'API_TESTS': """
->>> from django.newforms.models import inline_formset
+>>> from django.newforms.models import _inlineformset_factory
Child has two ForeignKeys to Parent, so if we don't specify which one to use
for the inline formset, we should get an exception.
->>> ifs = inline_formset(Parent, Child)
+>>> ifs = _inlineformset_factory(Parent, Child)
Traceback (most recent call last):
...
Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
@@ -29,14 +29,14 @@ class Child(models.Model):
These two should both work without a problem.
->>> ifs = inline_formset(Parent, Child, fk_name='mother')
->>> ifs = inline_formset(Parent, Child, fk_name='father')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='mother')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='father')
If we specify fk_name, but it isn't a ForeignKey from the child model to the
parent model, we should get an exception.
->>> ifs = inline_formset(Parent, Child, fk_name='school')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='school')
Traceback (most recent call last):
...
Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
@@ -45,7 +45,7 @@ class Child(models.Model):
If the field specified in fk_name is not a ForeignKey, we should get an
exception.
->>> ifs = inline_formset(Parent, Child, fk_name='test')
+>>> ifs = _inlineformset_factory(Parent, Child, fk_name='test')
Traceback (most recent call last):
...
Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
Please sign in to comment.
Something went wrong with that request. Please try again.