{% endif %}
+ {% if inline_admin_formset.deletable %}
{% trans "Delete" %}?
{% endif %}
{% for inline_admin_form in inline_admin_formset %}
@@ -45,7 +45,7 @@
{% endfor %}
{% endfor %}
- {% if inline_admin_formset.formset.deletable %}
{{ inline_admin_form.deletion_field.field }}
{% endif %}
+ {% if inline_admin_formset.deletable %}
{{ inline_admin_form.deletion_field.field }}
{% endif %}
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py
index 56179a9..d9b34cf 100644
--- a/django/newforms/formsets.py
+++ b/django/newforms/formsets.py
@@ -1,9 +1,16 @@
-from forms import Form
-from fields import IntegerField, BooleanField
+
+from warnings import warn
+
+from django.utils.datastructures import SortedDict
+from django.utils.translation import ugettext_lazy as _
+
+from forms import BaseForm, Form
+from fields import Field, IntegerField, BooleanField
+from options import FormSetOptions
from widgets import HiddenInput, Media
from util import ErrorList, ValidationError
-__all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
+__all__ = ('BaseFormSet', 'FormSet', 'formset_for_form', 'all_valid')
# special field names
FORM_COUNT_FIELD_NAME = 'COUNT'
@@ -20,6 +27,24 @@ class ManagementForm(Form):
self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs)
+class BaseFormSetMetaclass(type):
+ def __new__(cls, name, bases, attrs, **options):
+ fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
+ fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
+
+ # If this class is subclassing another FormSet, ad that FormSet's fields.
+ # Note that we loop over the bases in *reverse*. This is necessary in
+ # order to preserve the correct order of fields.
+ for base in bases[::-1]:
+ if hasattr(base, "base_fields"):
+ fields = base.base_fields.items() + fields
+ attrs["base_fields"] = SortedDict(fields)
+
+ opts = FormSetOptions(options and options or attrs.get("Meta", None))
+ attrs["_meta"] = opts
+
+ return type.__new__(cls, name, bases, attrs)
+
class BaseFormSet(object):
"""A collection of instances of the same Form class."""
@@ -37,25 +62,24 @@ class BaseFormSet(object):
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.required_forms = self.total_forms - self._meta.num_extra
+ self.change_form_count = self.total_forms - self._meta.num_extra
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.total_forms = self.required_forms + self._meta.num_extra
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
else:
self.change_form_count = 0
self.required_forms = 0
- self.total_forms = self.num_extra
+ self.total_forms = self._meta.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):
@@ -64,7 +88,7 @@ class BaseFormSet(object):
kwargs['data'] = self.data
if self.files:
kwargs['files'] = self.files
- add_form = FormClass(**kwargs)
+ add_form = self.get_form_class(i)(**kwargs)
self.add_fields(add_form, i)
add_forms.append(add_form)
self._add_forms = add_forms
@@ -73,7 +97,6 @@ class BaseFormSet(object):
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):
@@ -84,10 +107,10 @@ class BaseFormSet(object):
kwargs['files'] = self.files
if self.initial:
kwargs['initial'] = self.initial[i]
- change_form = FormClass(**kwargs)
+ change_form = self.get_form_class(i)(**kwargs)
self.add_fields(change_form, i)
change_forms.append(change_form)
- self._change_forms= change_forms
+ self._change_forms = change_forms
return self._change_forms
change_forms = property(_get_change_forms)
@@ -117,7 +140,7 @@ class BaseFormSet(object):
# Process change forms
for form in self.change_forms:
if form.is_valid():
- if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+ if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
self.deleted_data.append(form.cleaned_data)
else:
self.cleaned_data.append(form.cleaned_data)
@@ -144,7 +167,7 @@ class BaseFormSet(object):
add_errors.reverse()
errors.extend(add_errors)
# Sort cleaned_data if the formset is orderable.
- if self.orderable:
+ if self._meta.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:
@@ -167,12 +190,20 @@ class BaseFormSet(object):
via formset.non_form_errors()
"""
return self.cleaned_data
+
+ def get_form_class(self, index):
+ """
+ A hook to change a form class object.
+ """
+ FormClass = self._meta.form
+ FormClass.base_fields = self.base_fields
+ return FormClass
def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance."""
- if self.orderable:
+ if self._meta.orderable:
form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
- if self.deletable:
+ if self._meta.deletable:
form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
def add_prefix(self, index):
@@ -192,12 +223,21 @@ class BaseFormSet(object):
else:
return Media()
media = property(_get_media)
+
+class FormSet(BaseFormSet):
+ __metaclass__ = BaseFormSetMetaclass
-def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
+def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False,
+ deletable=False):
"""Return a FormSet for the given form class."""
- attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
- return type(form.__name__ + 'FormSet', (formset,), attrs)
-
+ warn("formset_for_form is deprecated, use FormSet instead.",
+ PendingDeprecationWarning,
+ stacklevel=3)
+ return BaseFormSetMetaclass(
+ form.__name__ + "FormSet", (formset,), form.base_fields,
+ form=form, num_extra=num_extra, orderable=orderable,
+ deletable=deletable)
+
def all_valid(formsets):
"""Returns true if every formset in formsets is valid."""
valid = True
diff --git a/django/newforms/models.py b/django/newforms/models.py
index e0f2cde..68da932 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -13,12 +13,14 @@ from django.core.exceptions import ImproperlyConfigured
from util import ValidationError, ErrorList
from forms import BaseForm
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
-from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
+from formsets import FormSetOptions, BaseFormSet, formset_for_form, DELETION_FIELD_NAME
+from options import ModelFormOptions, ModelFormSetOptions, InlineFormSetOptions
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
__all__ = (
'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
+ 'ModelFormSet', 'InlineFormset', 'modelform_for_model',
'formset_for_model', 'inline_formset',
'ModelChoiceField', 'ModelMultipleChoiceField',
)
@@ -207,15 +209,11 @@ def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda
field_list.append((f.name, formfield))
return SortedDict(field_list)
-class ModelFormOptions(object):
- def __init__(self, options=None):
- self.model = getattr(options, 'model', None)
- self.fields = getattr(options, 'fields', None)
- self.exclude = getattr(options, 'exclude', None)
-
class ModelFormMetaclass(type):
+ opts_class = ModelFormOptions
+
def __new__(cls, name, bases, attrs,
- formfield_callback=lambda f: f.formfield()):
+ formfield_callback=lambda f: f.formfield(), **options):
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
@@ -227,7 +225,7 @@ class ModelFormMetaclass(type):
fields = base.base_fields.items() + fields
declared_fields = SortedDict(fields)
- opts = ModelFormOptions(attrs.get('Meta', None))
+ opts = cls.opts_class(options and options or attrs.get('Meta', None))
attrs['_meta'] = opts
# Don't allow more than one Meta model defenition in bases. The fields
@@ -260,7 +258,7 @@ class ModelFormMetaclass(type):
else:
attrs['base_fields'] = declared_fields
return type.__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):
@@ -293,6 +291,14 @@ class BaseModelForm(BaseForm):
class ModelForm(BaseModelForm):
__metaclass__ = ModelFormMetaclass
+# this should really be named form_for_model.
+def modelform_for_model(model, form=ModelForm,
+ formfield_callback=lambda f: f.formfield(), **options):
+ opts = model._meta
+ options.update({"model": model})
+ return ModelFormMetaclass(opts.object_name + "ModelForm", (form,),
+ {}, formfield_callback, **options)
+
# Fields #####################################################################
@@ -407,47 +413,33 @@ class ModelMultipleChoiceField(ModelChoiceField):
# 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 ModelFormSetMetaclass(ModelFormMetaclass):
+ opts_class = ModelFormSetOptions
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):
kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
- self.queryset = qs
- kwargs['initial'] = [initial_data(obj) for obj in qs]
+ opts = self._meta
+ self.queryset = self.get_queryset(**kwargs)
+ initial_data = []
+ for obj in self.queryset:
+ initial_data.append(model_to_dict(obj, opts.fields, opts.exclude))
+ kwargs['initial'] = initial_data
super(BaseModelFormSet, self).__init__(**kwargs)
+
+ def get_queryset(self, **kwargs):
+ """
+ Hook to returning a queryset for this model.
+ """
+ return self._meta.model._default_manager.all()
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)
+ return save_instance(form, self._meta.model(), commit=commit)
def save_instance(self, form, instance, commit=True):
"""Saves and returns an existing model instance for the given form."""
@@ -464,12 +456,13 @@ class BaseModelFormSet(BaseFormSet):
return []
# Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
existing_objects = {}
+ opts = self._meta
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 = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]]
+ if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
obj.delete()
else:
saved_instances.append(self.save_instance(form, obj, commit=commit))
@@ -477,25 +470,29 @@ class BaseModelFormSet(BaseFormSet):
def save_new_objects(self, commit=True):
new_objects = []
+ opts = self._meta
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]:
+ if opts.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
+ self._pk_field_name = self._meta.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):
+class ModelFormSet(BaseModelFormSet):
+ __metaclass__ = ModelFormSetMetaclass
+
+def formset_for_model(model, formset=BaseModelFormSet,
+ formfield_callback=lambda f: f.formfield(), **options):
"""
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
@@ -504,80 +501,101 @@ def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formf
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
+ opts = model._meta
+ options.update({"model": model})
+ return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,),
+ {}, **options)
+
+class InlineFormSetMetaclass(ModelFormSetMetaclass):
+ opts_class = InlineFormSetOptions
+
+ def __new__(cls, name, bases, attrs,
+ formfield_callback=lambda f: f.formfield(), **options):
+ formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs,
+ formfield_callback, **options)
+ # If this isn't a subclass of InlineFormset, don't do anything special.
+ try:
+ if not filter(lambda b: issubclass(b, InlineFormset), bases):
+ return formset
+ except NameError:
+ # 'InlineFormset' isn't defined yet, meaning we're looking at
+ # Django's own InlineFormset class, defined below.
+ return formset
+ opts = formset._meta
+ # resolve the foreign key
+ fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name)
+ # remove the fk from base_fields to keep it transparent to the form.
+ try:
+ del formset.base_fields[fk.name]
+ except KeyError:
+ pass
+ formset.fk = fk
+ return formset
+
+ def _resolve_foreign_key(cls, 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 a 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
+ resolve_foreign_key = classmethod(_resolve_foreign_key)
-class InlineFormset(BaseModelFormSet):
+class BaseInlineFormSet(BaseModelFormSet):
"""A formset for child objects related to a parent."""
- def __init__(self, instance, data=None, files=None):
+ def __init__(self, *args, **kwargs):
from django.db.models.fields.related import RelatedObject
- self.instance = instance
+ opts = self._meta
+ self.instance = kwargs.pop("instance", None)
# 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)
+ rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name()
+ kwargs["prefix"] = rel_name
+ super(BaseInlineFormSet, self).__init__(*args, **kwargs)
- def get_queryset(self):
+ def get_queryset(self, **kwargs):
"""
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)
+ queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
+ return queryset.filter(**{self.fk.name: self.instance})
def save_new(self, form, commit=True):
kwargs = {self.fk.get_attname(): self.instance.pk}
- new_obj = self.model(**kwargs)
+ new_obj = self._meta.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
+class InlineFormset(BaseInlineFormSet):
+ __metaclass__ = InlineFormSetMetaclass
-def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
+def inline_formset(parent_model, model, formset=InlineFormset,
+ formfield_callback=lambda f: f.formfield(), **options):
"""
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
+ opts = model._meta
+ options.update({"parent_model": parent_model, "model": model})
+ return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,),
+ {}, formfield_callback, **options)
diff --git a/django/newforms/options.py b/django/newforms/options.py
new file mode 100644
index 0000000..a4565e4
--- /dev/null
+++ b/django/newforms/options.py
@@ -0,0 +1,50 @@
+
+from forms import BaseForm
+
+class BaseFormOptions(object):
+ """
+ The base class for all options that are associated to a form object.
+ """
+ def __init__(self, options=None):
+ self.fields = self._dynamic_attribute(options, "fields")
+ self.exclude = self._dynamic_attribute(options, "exclude")
+
+ def _dynamic_attribute(self, obj, key, default=None):
+ try:
+ return getattr(obj, key)
+ except AttributeError:
+ try:
+ return obj[key]
+ except (TypeError, KeyError):
+ # key doesnt exist in obj or obj is None
+ return default
+
+class ModelFormOptions(BaseFormOptions):
+ """
+ Encapsulates the options on a ModelForm class.
+ """
+ def __init__(self, options=None):
+ self.model = self._dynamic_attribute(options, "model")
+ super(ModelFormOptions, self).__init__(options)
+
+class FormSetOptions(BaseFormOptions):
+ """
+ Encapsulates the options on a FormSet class.
+ """
+ def __init__(self, options=None):
+ self.form = self._dynamic_attribute(options, "form", BaseForm)
+ self.num_extra = self._dynamic_attribute(options, "num_extra", 1)
+ self.orderable = self._dynamic_attribute(options, "orderable", False)
+ self.deletable = self._dynamic_attribute(options, "deletable", False)
+ super(FormSetOptions, self).__init__(options)
+
+class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
+ pass
+
+class InlineFormSetOptions(ModelFormSetOptions):
+ def __init__(self, options=None):
+ super(InlineFormSetOptions, self).__init__(options)
+ self.parent_model = self._dynamic_attribute(options, "parent_model")
+ self.fk_name = self._dynamic_attribute(options, "fk_name")
+ self.deletable = self._dynamic_attribute(options, "deletable", True)
+
\ No newline at end of file
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
index 19bdeed..d1e72ea 100644
--- a/tests/modeltests/model_formsets/models.py
+++ b/tests/modeltests/model_formsets/models.py
@@ -16,12 +16,35 @@ class Book(models.Model):
__test__ = {'API_TESTS': """
->>> from django.newforms.models import formset_for_model
+>>> from django import newforms as forms
+>>> from django.newforms.models import formset_for_model, ModelFormSet
->>> qs = Author.objects.all()
->>> AuthorFormSet = formset_for_model(Author, extra=3)
+A bare bones verion.
->>> formset = AuthorFormSet(qs)
+>>> class AuthorFormSet(ModelFormSet):
+... class Meta:
+... model = Author
+>>> AuthorFormSet.base_fields.keys()
+['name']
+
+Extra fields.
+
+>>> class AuthorFormSet(ModelFormSet):
+... published = forms.BooleanField()
+...
+... class Meta:
+... model = Author
+>>> AuthorFormSet.base_fields.keys()
+['name', 'published']
+
+Lets create a formset that is bound to a model.
+
+>>> class AuthorFormSet(ModelFormSet):
+... class Meta:
+... model = Author
+... num_extra = 3
+
+>>> formset = AuthorFormSet()
>>> for form in formset.forms:
... print form.as_p()
@@ -35,7 +58,7 @@ __test__ = {'API_TESTS': """
... 'form-2-name': '',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data)
>>> formset.is_valid()
True
@@ -49,14 +72,21 @@ Charles Baudelaire
Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
-authors with an extra form to add him. This time we'll use formset_for_queryset.
-We *could* use formset_for_queryset to restrict the Author objects we edit,
-but in that case we'll use it to display them in alphabetical order by name.
-
->>> qs = Author.objects.order_by('name')
->>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False)
-
->>> formset = AuthorFormSet(qs)
+authors with an extra form to add him. When subclassing ModelFormSet you can
+override the get_queryset method to return any queryset we like, but in this
+case we'll use it to display it in alphabetical order by name.
+
+>>> class AuthorFormSet(ModelFormSet):
+... class Meta:
+... model = Author
+... num_extra = 1
+... deletable = False
+...
+... def get_queryset(self, **kwargs):
+... qs = super(AuthorFormSet, self).get_queryset(**kwargs)
+... return qs.order_by('name')
+
+>>> formset = AuthorFormSet()
>>> for form in formset.forms:
... print form.as_p()
@@ -73,7 +103,7 @@ but in that case we'll use it to display them in alphabetical order by name.
... 'form-2-name': 'Paul Verlaine',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data)
>>> formset.is_valid()
True
@@ -90,10 +120,17 @@ Paul Verlaine
This probably shouldn't happen, but it will. If an add form was marked for
deltetion, make sure we don't save that form.
->>> qs = Author.objects.order_by('name')
->>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
-
->>> formset = AuthorFormSet(qs)
+>>> class AuthorFormSet(ModelFormSet):
+... class Meta:
+... model = Author
+... num_extra = 1
+... deletable = True
+...
+... def get_queryset(self, **kwargs):
+... qs = super(AuthorFormSet, self).get_queryset(**kwargs)
+... return qs.order_by('name')
+
+>>> formset = AuthorFormSet()
>>> for form in formset.forms:
... print form.as_p()
@@ -117,7 +154,7 @@ deltetion, make sure we don't save that form.
... 'form-3-DELETE': 'on',
... }
->>> formset = AuthorFormSet(qs, data=data)
+>>> formset = AuthorFormSet(data)
>>> formset.is_valid()
True
@@ -134,12 +171,18 @@ Paul Verlaine
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 inline_formset, InlineFormset
+
+>>> class AuthorBooksFormSet(InlineFormset):
+... class Meta:
+... parent_model = Author
+... model = Book
+... num_extra = 3
+... deletable = False
->>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=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()
@@ -153,7 +196,7 @@ admin system's edit inline functionality works.
... 'book_set-2-title': '',
... }
->>> formset = AuthorBooksFormSet(author, data=data)
+>>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid()
True
@@ -169,10 +212,16 @@ Now that we've added a book to Charles Baudelaire, let's try adding another
one. This time though, an edit form will be available for every existing
book.
->>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
+>>> class AuthorBooksFormSet(InlineFormset):
+... class Meta:
+... parent_model = Author
+... model = Book
+... num_extra = 2
+... deletable = False
+
>>> author = Author.objects.get(name='Charles Baudelaire')
->>> formset = AuthorBooksFormSet(author)
+>>> formset = AuthorBooksFormSet(instance=author)
>>> for form in formset.forms:
... print form.as_p()
@@ -187,7 +236,7 @@ book.
... 'book_set-2-title': '',
... }
->>> formset = AuthorBooksFormSet(author, data=data)
+>>> formset = AuthorBooksFormSet(data, instance=author)
>>> formset.is_valid()
True
diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py
index a6da2fe..f5cb2ee 100644
--- a/tests/regressiontests/forms/formsets.py
+++ b/tests/regressiontests/forms/formsets.py
@@ -2,18 +2,17 @@
formset_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.
+FormSet allows us to use multiple instance of the same form on 1 page. Create
+the formset as you would a regular form by defining the fields declaratively.
>>> from django.newforms import Form, CharField, IntegerField, ValidationError
->>> from django.newforms.formsets import formset_for_form, BaseFormSet
+>>> from django.newforms import BooleanField
+>>> from django.newforms.formsets import formset_for_form, BaseFormSet, FormSet
->>> class Choice(Form):
+>>> class ChoiceFormSet(FormSet):
... choice = CharField()
... votes = IntegerField()
->>> ChoiceFormSet = formset_for_form(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,
@@ -145,15 +144,31 @@ False
>>> formset.errors
[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}]
+# Subclassing a FormSet class #################################################
+
+We can subclass a FormSet to add addition fields to an already exisiting
+FormSet.
+
+>>> class SecondChoiceFormSet(ChoiceFormSet):
+... is_public = BooleanField()
+
+>>> formset = SecondChoiceFormSet(auto_id=False, prefix="choices")
+>>> for form in formset.forms:
+... print form.as_ul()
+
Choice:
+
Votes:
+
Is public:
# 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.
+We can also display more than 1 empty form at a time. To do so, create an inner
+Meta class with an attribute num_extra.
->>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)
+>>> class NumExtraChoiceFormSet(ChoiceFormSet):
+... class Meta:
+... num_extra = 3
->>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
+>>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms:
... print form.as_ul()
Choice:
@@ -177,7 +192,7 @@ number of forms to be completed.
... 'choices-2-votes': '',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
@@ -196,7 +211,7 @@ We can just fill out one of the forms.
... 'choices-2-votes': '',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
@@ -215,7 +230,7 @@ And once again, if we try to partially complete a form, validation will fail.
... 'choices-2-votes': '',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
@@ -226,7 +241,7 @@ The num_extra argument also works when the formset is pre-filled with initial
data.
>>> initial = [{'choice': u'Calexico', 'votes': 100}]
->>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
+>>> formset = NumExtraChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
... print form.as_ul()
Choice:
@@ -254,7 +269,7 @@ get an error.
... 'choices-3-votes': '',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
@@ -263,15 +278,17 @@ False
# 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
+We can easily add deletion ability to a FormSet by setting deletable to True
+in the inner Meta class. 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
->>> ChoiceFormSet = formset_for_form(Choice, deletable=True)
+>>> class DeletableChoiceFormSet(ChoiceFormSet):
+... class Meta:
+... deletable = True
>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
->>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
+>>> formset = DeletableChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
... print form.as_ul()
Choice:
@@ -300,7 +317,7 @@ To delete something, we just need to set that form's special delete field to
... 'choices-2-DELETE': '',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
@@ -310,18 +327,20 @@ True
# 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
+We can also add ordering ability to a FormSet by setting orderable to True in
+the inner Meta class. This will add a integer field to each form instance. When
form validation succeeds, formset.cleaned_data 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)
+>>> class OrderableChoiceFormSet(ChoiceFormSet):
+... class Meta:
+... orderable = True
>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
->>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
+>>> formset = OrderableChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
... print form.as_ul()
Choice:
@@ -347,7 +366,7 @@ something at the front of the list, you'd need to set it's order to 0.
... 'choices-2-ORDER': '0',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = OrderableChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> for cleaned_data in formset.cleaned_data:
@@ -359,15 +378,20 @@ True
# FormSets with ordering + deletion ###########################################
Let's try throwing ordering and deletion into the same form.
+TODO: Perhaps handle Meta class inheritance so you can subclass
+OrderableChoiceFormSet and DeletableChoiceFormSet?
->>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)
+>>> class MixedChoiceFormSet(ChoiceFormSet):
+... class Meta:
+... orderable = True
+... deletable = True
>>> initial = [
... {'choice': u'Calexico', 'votes': 100},
... {'choice': u'Fergie', 'votes': 900},
... {'choice': u'The Decemberists', 'votes': 500},
... ]
->>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
+>>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
... print form.as_ul()
Choice:
@@ -409,7 +433,7 @@ Let's delete Fergie, and put The Decemberists ahead of Calexico.
... 'choices-3-DELETE': '',
... }
->>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset = MixedChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> for cleaned_data in formset.cleaned_data:
@@ -428,15 +452,13 @@ particular form. It follows the same pattern as the clean hook on Forms.
Let's define a FormSet that takes a list of favorite drinks, but raises am
error if there are any duplicates.
->>> class FavoriteDrinkForm(Form):
+>>> class FavoriteDrinksFormSet(FormSet):
... name = CharField()
-...
-
->>> class FavoriteDrinksFormSet(BaseFormSet):
-... form_class = FavoriteDrinkForm
-... num_extra = 2
-... orderable = False
-... deletable = False
+...
+... class Meta:
+... num_extra = 2
+... orderable = False
+... deletable = False
...
... def clean(self):
... seen_drinks = []
diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py
index f84be84..180bba1 100644
--- a/tests/regressiontests/inline_formsets/models.py
+++ b/tests/regressiontests/inline_formsets/models.py
@@ -21,7 +21,7 @@ __test__ = {'API_TESTS': """
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)
+>>> inline_formset(Parent, Child)()
Traceback (most recent call last):
...
Exception: has more than 1 ForeignKey to
@@ -29,14 +29,14 @@ Exception: has more than
These two should both work without a problem.
->>> ifs = inline_formset(Parent, Child, fk_name='mother')
->>> ifs = inline_formset(Parent, Child, fk_name='father')
+>>> ifs = inline_formset(Parent, Child, fk_name='mother')()
+>>> ifs = inline_formset(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')
+>>> inline_formset(Parent, Child, fk_name='school')()
Traceback (most recent call last):
...
Exception: fk_name 'school' is not a ForeignKey to
@@ -45,7 +45,7 @@ Exception: fk_name 'school' is not a ForeignKey to >> ifs = inline_formset(Parent, Child, fk_name='test')
+>>> inline_formset(Parent, Child, fk_name='test')()
Traceback (most recent call last):
...
Exception: has no field named 'test'