Ticket #6241: formset_refactor_5.diff

File formset_refactor_5.diff, 61.2 KB (added by Brian Rosner, 16 years ago)

use ModelForm in ModelAdmin. refactors formset code.

  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index c3289ea..70640c1 100644
    a b class ModelAdmin(BaseModelAdmin):  
    342342            fields = flatten_fieldsets(self.declared_fieldsets)
    343343        else:
    344344            fields = None
    345         return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
     345        return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    346346
    347347    def form_change(self, request, obj):
    348348        """
    class ModelAdmin(BaseModelAdmin):  
    352352            fields = flatten_fieldsets(self.declared_fieldsets)
    353353        else:
    354354            fields = None
    355         return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)
     355        return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    356356
    357357    def save_add(self, request, model, form, formsets, post_url_continue):
    358358        """
    class ModelAdmin(BaseModelAdmin):  
    492492            # Object list will give 'Permission Denied', so go back to admin home
    493493            post_url = '../../../'
    494494
    495         ModelForm = self.form_add(request)
     495        Form = self.form_add(request)
    496496        inline_formsets = []
    497497        obj = self.model()
    498498        if request.method == 'POST':
    499             form = ModelForm(request.POST, request.FILES)
     499            form = Form(request.POST, request.FILES)
    500500            for FormSet in self.formsets_add(request):
    501                 inline_formset = FormSet(obj, data=request.POST, files=request.FILES)
     501                inline_formset = FormSet(request.POST, request.FILES)
    502502                inline_formsets.append(inline_formset)
    503503            if all_valid(inline_formsets) and form.is_valid():
    504504                return self.save_add(request, model, form, inline_formsets, '../%s/')
    505505        else:
    506             form = ModelForm(initial=request.GET)
     506            form = Form(initial=request.GET)
    507507            for FormSet in self.formsets_add(request):
    508                 inline_formset = FormSet(obj)
     508                inline_formset = FormSet()
    509509                inline_formsets.append(inline_formset)
    510510
    511511        adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
    class ModelAdmin(BaseModelAdmin):  
    552552        if request.POST and request.POST.has_key("_saveasnew"):
    553553            return self.add_view(request, form_url='../../add/')
    554554
    555         ModelForm = self.form_change(request, obj)
     555        Form = self.form_change(request, obj)
    556556        inline_formsets = []
    557557        if request.method == 'POST':
    558             form = ModelForm(request.POST, request.FILES)
     558            form = Form(request.POST, request.FILES, instance=obj)
    559559            for FormSet in self.formsets_change(request, obj):
    560                 inline_formset = FormSet(obj, request.POST, request.FILES)
     560                inline_formset = FormSet(request.POST, request.FILES, instance=obj)
    561561                inline_formsets.append(inline_formset)
    562562
    563563            if all_valid(inline_formsets) and form.is_valid():
    564564                return self.save_change(request, model, form, inline_formsets)
    565565        else:
    566             form = ModelForm()
     566            form = Form(instance=obj, initial=request.GET)
    567567            for FormSet in self.formsets_change(request, obj):
    568                 inline_formset = FormSet(obj)
     568                inline_formset = FormSet(instance=obj)
    569569                inline_formsets.append(inline_formset)
    570570
    571571        ## Populate the FormWrapper.
    class InlineModelAdmin(BaseModelAdmin):  
    755755    def fieldsets_add(self, request):
    756756        if self.declared_fieldsets:
    757757            return self.declared_fieldsets
    758         form = self.formset_add(request).form_class
    759         return [(None, {'fields': form.base_fields.keys()})]
     758        formset = self.formset_add(request)
     759        return [(None, {'fields': formset.base_fields.keys()})]
    760760
    761761    def fieldsets_change(self, request, obj):
    762762        if self.declared_fieldsets:
    763763            return self.declared_fieldsets
    764         form = self.formset_change(request, obj).form_class
    765         return [(None, {'fields': form.base_fields.keys()})]
     764        formset = self.formset_change(request, obj)
     765        return [(None, {'fields': formset.base_fields.keys()})]
    766766
    767767class StackedInline(InlineModelAdmin):
    768768    template = 'admin/edit_inline/stacked.html'
    class InlineAdminFormSet(object):  
    778778        self.opts = inline
    779779        self.formset = formset
    780780        self.fieldsets = fieldsets
     781        # place orderable and deletable here since _meta is inaccesible in the
     782        # templates.
     783        self.orderable = formset._meta.orderable
     784        self.deletable = formset._meta.deletable
    781785
    782786    def __iter__(self):
    783787        for form, original in zip(self.formset.change_forms, self.formset.get_queryset()):
    class InlineAdminFormSet(object):  
    787791
    788792    def fields(self):
    789793        for field_name in flatten_fieldsets(self.fieldsets):
    790             yield self.formset.form_class.base_fields[field_name]
     794            yield self.formset.base_fields[field_name]
    791795
    792796class InlineAdminForm(AdminForm):
    793797    """
  • django/contrib/admin/templates/admin/edit_inline/tabular.html

    diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
    index 16bb14d..c91bb0e 100644
    a b  
    1111         <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst|escape }}</th>
    1212        {% endif %}
    1313     {% endfor %}
    14      {% if inline_admin_formset.formset.deletable %}<th>{% trans "Delete" %}?</th>{% endif %}
     14     {% if inline_admin_formset.deletable %}<th>{% trans "Delete" %}?</th>{% endif %}
    1515     </tr></thead>
    1616   
    1717     {% for inline_admin_form in inline_admin_formset %}
     
    4545          {% endfor %}
    4646        {% endfor %}
    4747               
    48         {% if inline_admin_formset.formset.deletable %}<td class="delete">{{ inline_admin_form.deletion_field.field }}</td>{% endif %}
     48        {% if inline_admin_formset.deletable %}<td class="delete">{{ inline_admin_form.deletion_field.field }}</td>{% endif %}
    4949       
    5050        </tr>
    5151
  • django/newforms/formsets.py

    diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py
    index 56179a9..d9b34cf 100644
    a b  
    1 from forms import Form
    2 from fields import IntegerField, BooleanField
     1
     2from warnings import warn
     3
     4from django.utils.datastructures import SortedDict
     5from django.utils.translation import ugettext_lazy as _
     6
     7from forms import BaseForm, Form
     8from fields import Field, IntegerField, BooleanField
     9from options import FormSetOptions
    310from widgets import HiddenInput, Media
    411from util import ErrorList, ValidationError
    512
    6 __all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
     13__all__ = ('BaseFormSet', 'FormSet', 'formset_for_form', 'all_valid')
    714
    815# special field names
    916FORM_COUNT_FIELD_NAME = 'COUNT'
    class ManagementForm(Form):  
    2027        self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
    2128        super(ManagementForm, self).__init__(*args, **kwargs)
    2229
     30class BaseFormSetMetaclass(type):
     31    def __new__(cls, name, bases, attrs, **options):
     32        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
     33        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
     34       
     35        # If this class is subclassing another FormSet, ad that FormSet's fields.
     36        # Note that we loop over the bases in *reverse*. This is necessary in
     37        # order to preserve the correct order of fields.
     38        for base in bases[::-1]:
     39            if hasattr(base, "base_fields"):
     40                fields = base.base_fields.items() + fields
     41        attrs["base_fields"] = SortedDict(fields)
     42       
     43        opts = FormSetOptions(options and options or attrs.get("Meta", None))
     44        attrs["_meta"] = opts
     45       
     46        return type.__new__(cls, name, bases, attrs)
     47
    2348class BaseFormSet(object):
    2449    """A collection of instances of the same Form class."""
    2550
    class BaseFormSet(object):  
    3762            self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
    3863            if self.management_form.is_valid():
    3964                self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME]
    40                 self.required_forms = self.total_forms - self.num_extra
    41                 self.change_form_count = self.total_forms - self.num_extra
     65                self.required_forms = self.total_forms - self._meta.num_extra
     66                self.change_form_count = self.total_forms - self._meta.num_extra
    4267            else:
    4368                # not sure that ValidationError is the best thing to raise here
    4469                raise ValidationError('ManagementForm data is missing or has been tampered with')
    4570        elif initial:
    4671            self.change_form_count = len(initial)
    4772            self.required_forms = len(initial)
    48             self.total_forms = self.required_forms + self.num_extra
     73            self.total_forms = self.required_forms + self._meta.num_extra
    4974            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
    5075        else:
    5176            self.change_form_count = 0
    5277            self.required_forms = 0
    53             self.total_forms = self.num_extra
     78            self.total_forms = self._meta.num_extra
    5479            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
    5580
    5681    def _get_add_forms(self):
    5782        """Return a list of all the add forms in this ``FormSet``."""
    58         FormClass = self.form_class
    5983        if not hasattr(self, '_add_forms'):
    6084            add_forms = []
    6185            for i in range(self.change_form_count, self.total_forms):
    class BaseFormSet(object):  
    6488                    kwargs['data'] = self.data
    6589                if self.files:
    6690                    kwargs['files'] = self.files
    67                 add_form = FormClass(**kwargs)
     91                add_form = self.get_form_class(i)(**kwargs)
    6892                self.add_fields(add_form, i)
    6993                add_forms.append(add_form)
    7094            self._add_forms = add_forms
    class BaseFormSet(object):  
    7397
    7498    def _get_change_forms(self):
    7599        """Return a list of all the change forms in this ``FormSet``."""
    76         FormClass = self.form_class
    77100        if not hasattr(self, '_change_forms'):
    78101            change_forms = []
    79102            for i in range(0, self.change_form_count):
    class BaseFormSet(object):  
    84107                    kwargs['files'] = self.files
    85108                if self.initial:
    86109                    kwargs['initial'] = self.initial[i]
    87                 change_form = FormClass(**kwargs)
     110                change_form = self.get_form_class(i)(**kwargs)
    88111                self.add_fields(change_form, i)
    89112                change_forms.append(change_form)
    90             self._change_forms= change_forms
     113            self._change_forms = change_forms
    91114        return self._change_forms
    92115    change_forms = property(_get_change_forms)
    93116
    class BaseFormSet(object):  
    117140        # Process change forms
    118141        for form in self.change_forms:
    119142            if form.is_valid():
    120                 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     143                if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    121144                    self.deleted_data.append(form.cleaned_data)
    122145                else:
    123146                    self.cleaned_data.append(form.cleaned_data)
    class BaseFormSet(object):  
    144167        add_errors.reverse()
    145168        errors.extend(add_errors)
    146169        # Sort cleaned_data if the formset is orderable.
    147         if self.orderable:
     170        if self._meta.orderable:
    148171            self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
    149172        # Give self.clean() a chance to do validation
    150173        try:
    class BaseFormSet(object):  
    167190        via formset.non_form_errors()
    168191        """
    169192        return self.cleaned_data
     193   
     194    def get_form_class(self, index):
     195        """
     196        A hook to change a form class object.
     197        """
     198        FormClass = self._meta.form
     199        FormClass.base_fields = self.base_fields
     200        return FormClass
    170201
    171202    def add_fields(self, form, index):
    172203        """A hook for adding extra fields on to each form instance."""
    173         if self.orderable:
     204        if self._meta.orderable:
    174205            form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
    175         if self.deletable:
     206        if self._meta.deletable:
    176207            form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
    177208
    178209    def add_prefix(self, index):
    class BaseFormSet(object):  
    192223        else:
    193224            return Media()
    194225    media = property(_get_media)
     226
     227class FormSet(BaseFormSet):
     228    __metaclass__ = BaseFormSetMetaclass
    195229   
    196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
     230def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False,
     231                     deletable=False):
    197232    """Return a FormSet for the given form class."""
    198     attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
    199     return type(form.__name__ + 'FormSet', (formset,), attrs)
    200 
     233    warn("formset_for_form is deprecated, use FormSet instead.",
     234         PendingDeprecationWarning,
     235         stacklevel=3)
     236    return BaseFormSetMetaclass(
     237        form.__name__ + "FormSet", (formset,), form.base_fields,
     238        form=form, num_extra=num_extra, orderable=orderable,
     239        deletable=deletable)
     240       
    201241def all_valid(formsets):
    202242    """Returns true if every formset in formsets is valid."""
    203243    valid = True
  • django/newforms/models.py

    diff --git a/django/newforms/models.py b/django/newforms/models.py
    index 3c9b43d..054e0f3 100644
    a b from django.core.exceptions import ImproperlyConfigured  
    1313from util import ValidationError, ErrorList
    1414from forms import BaseForm
    1515from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
    16 from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
     16from formsets import FormSetOptions, BaseFormSet, formset_for_form, DELETION_FIELD_NAME
     17from options import ModelFormOptions, ModelFormSetOptions, InlineFormSetOptions
    1718from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
    1819
    1920__all__ = (
    2021    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    2122    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
     23    'ModelFormSet', 'InlineFormset', 'modelform_for_model',
    2224    'formset_for_model', 'inline_formset',
    2325    'ModelChoiceField', 'ModelMultipleChoiceField',
    2426)
    def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda  
    209211            field_list.append((f.name, formfield))
    210212    return SortedDict(field_list)
    211213
    212 class ModelFormOptions(object):
    213     def __init__(self, options=None):
    214         self.model = getattr(options, 'model', None)
    215         self.fields = getattr(options, 'fields', None)
    216         self.exclude = getattr(options, 'exclude', None)
    217 
    218214class ModelFormMetaclass(type):
    219     def __new__(cls, name, bases, attrs):
    220         # TODO: no way to specify formfield_callback yet, do we need one, or
    221         # should it be a special case for the admin?
     215    opts_class = ModelFormOptions
     216   
     217    def __new__(cls, name, bases, attrs,
     218                formfield_callback=lambda f: f.formfield(), **options):
    222219        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
    223220        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
    224221
    class ModelFormMetaclass(type):  
    230227                fields = base.base_fields.items() + fields
    231228        declared_fields = SortedDict(fields)
    232229
    233         opts = ModelFormOptions(attrs.get('Meta', None))
     230        opts = cls.opts_class(options and options or attrs.get('Meta', None))
    234231        attrs['_meta'] = opts
    235232
    236233        # Don't allow more than one Meta model defenition in bases. The fields
    class ModelFormMetaclass(type):  
    255252                base_model = getattr(base_opts, 'model', None)
    256253                if base_model is not None:
    257254                    raise ImproperlyConfigured('%s defines more than one model.' % name)
    258             model_fields = fields_for_model(opts.model, opts.fields, opts.exclude)
     255            model_fields = fields_for_model(opts.model, opts.fields, opts.exclude, formfield_callback)
    259256            # fields declared in base classes override fields from the model
    260257            model_fields.update(declared_fields)
    261258            attrs['base_fields'] = model_fields
    262259        else:
    263260            attrs['base_fields'] = declared_fields
    264261        return type.__new__(cls, name, bases, attrs)
    265 
     262       
    266263class BaseModelForm(BaseForm):
    267     def __init__(self, instance, data=None, files=None, auto_id='id_%s', prefix=None,
    268                  initial=None, error_class=ErrorList, label_suffix=':'):
     264    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
     265                 initial=None, error_class=ErrorList, label_suffix=':', instance=None):
    269266        self.instance = instance
    270267        opts = self._meta
    271         object_data = model_to_dict(instance, opts.fields, opts.exclude)
     268        if instance is None:
     269            # if we didn't get an instance, instantiate a new one
     270            self.instance = opts.model()
     271            object_data = {}
     272        else:
     273            self.instance = instance
     274            object_data = model_to_dict(instance, opts.fields, opts.exclude)
    272275        # if initial was provided, it should override the values from instance
    273276        if initial is not None:
    274277            object_data.update(initial)
    class BaseModelForm(BaseForm):  
    290293class ModelForm(BaseModelForm):
    291294    __metaclass__ = ModelFormMetaclass
    292295
     296# this should really be named form_for_model.
     297def modelform_for_model(model, form=ModelForm,
     298                        formfield_callback=lambda f: f.formfield(), **options):
     299    opts = model._meta
     300    options.update({"model": model})
     301    return ModelFormMetaclass(opts.object_name + "ModelForm", (form,),
     302                              {}, formfield_callback, **options)
     303
    293304
    294305# Fields #####################################################################
    295306
    class ModelMultipleChoiceField(ModelChoiceField):  
    404415
    405416# Model-FormSet integration ###################################################
    406417
    407 def initial_data(instance, fields=None):
    408     """
    409     Return a dictionary from data in ``instance`` that is suitable for
    410     use as a ``Form`` constructor's ``initial`` argument.
    411 
    412     Provide ``fields`` to specify the names of specific fields to return.
    413     All field values in the instance will be returned if ``fields`` is not
    414     provided.
    415     """
    416     # avoid a circular import
    417     from django.db.models.fields.related import ManyToManyField
    418     opts = instance._meta
    419     initial = {}
    420     for f in opts.fields + opts.many_to_many:
    421         if not f.editable:
    422             continue
    423         if fields and not f.name in fields:
    424             continue
    425         if isinstance(f, ManyToManyField):
    426             # MultipleChoiceWidget needs a list of ints, not object instances.
    427             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
    428         else:
    429             initial[f.name] = f.value_from_object(instance)
    430     return initial
     418class ModelFormSetMetaclass(ModelFormMetaclass):
     419    opts_class = ModelFormSetOptions
    431420
    432421class BaseModelFormSet(BaseFormSet):
    433422    """
    434423    A ``FormSet`` for editing a queryset and/or adding new objects to it.
    435424    """
    436     model = None
    437     queryset = None
    438425
    439     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):
     426    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None):
    440427        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
    441         self.queryset = qs
    442         kwargs['initial'] = [initial_data(obj) for obj in qs]
     428        opts = self._meta
     429        self.queryset = self.get_queryset(**kwargs)
     430        initial_data = []
     431        for obj in self.queryset:
     432            initial_data.append(model_to_dict(obj, opts.fields, opts.exclude))
     433        kwargs['initial'] = initial_data
    443434        super(BaseModelFormSet, self).__init__(**kwargs)
     435   
     436    def get_queryset(self, **kwargs):
     437        """
     438        Hook to returning a queryset for this model.
     439        """
     440        return self._meta.model._default_manager.all()
    444441
    445442    def save_new(self, form, commit=True):
    446443        """Saves and returns a new model instance for the given form."""
    447         return save_instance(form, self.model(), commit=commit)
     444        return save_instance(form, self._meta.model(), commit=commit)
    448445
    449446    def save_instance(self, form, instance, commit=True):
    450447        """Saves and returns an existing model instance for the given form."""
    class BaseModelFormSet(BaseFormSet):  
    461458            return []
    462459        # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
    463460        existing_objects = {}
     461        opts = self._meta
    464462        for obj in self.queryset:
    465463            existing_objects[obj.pk] = obj
    466464        saved_instances = []
    467465        for form in self.change_forms:
    468             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
    469             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     466            obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]]
     467            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    470468                obj.delete()
    471469            else:
    472470                saved_instances.append(self.save_instance(form, obj, commit=commit))
    class BaseModelFormSet(BaseFormSet):  
    474472
    475473    def save_new_objects(self, commit=True):
    476474        new_objects = []
     475        opts = self._meta
    477476        for form in self.add_forms:
    478477            if form.is_empty():
    479478                continue
    480479            # If someone has marked an add form for deletion, don't save the
    481480            # object. At some point it would be nice if we didn't display
    482481            # the deletion widget for add forms.
    483             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     482            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    484483                continue
    485484            new_objects.append(self.save_new(form, commit=commit))
    486485        return new_objects
    487486
    488487    def add_fields(self, form, index):
    489488        """Add a hidden field for the object's primary key."""
    490         self._pk_field_name = self.model._meta.pk.attname
     489        self._pk_field_name = self._meta.model._meta.pk.attname
    491490        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    492491        super(BaseModelFormSet, self).add_fields(form, index)
    493492
    494 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
    495                       formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
     493class ModelFormSet(BaseModelFormSet):
     494    __metaclass__ = ModelFormSetMetaclass
     495
     496def formset_for_model(model, formset=BaseModelFormSet,
     497                      formfield_callback=lambda f: f.formfield(), **options):
    496498    """
    497499    Returns a FormSet class for the given Django model class. This FormSet
    498500    will contain change forms for every instance of the given model as well
    def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formf  
    501503    This is essentially the same as ``formset_for_queryset``, but automatically
    502504    uses the model's default manager to determine the queryset.
    503505    """
    504     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
    505     FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    506     FormSet.model = model
    507     return FormSet
     506    opts = model._meta
     507    options.update({"model": model})
     508    return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,),
     509                                 {}, **options)
     510
     511class InlineFormSetMetaclass(ModelFormSetMetaclass):
     512    opts_class = InlineFormSetOptions
     513   
     514    def __new__(cls, name, bases, attrs,
     515                formfield_callback=lambda f: f.formfield(), **options):
     516        formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs,
     517            formfield_callback, **options)
     518        # If this isn't a subclass of InlineFormset, don't do anything special.
     519        try:
     520            if not filter(lambda b: issubclass(b, InlineFormset), bases):
     521                return formset
     522        except NameError:
     523            # 'InlineFormset' isn't defined yet, meaning we're looking at
     524            # Django's own InlineFormset class, defined below.
     525            return formset
     526        opts = formset._meta
     527        # resolve the foreign key
     528        fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name)
     529        # remove the fk from base_fields to keep it transparent to the form.
     530        try:
     531            del formset.base_fields[fk.name]
     532        except KeyError:
     533            pass
     534        formset.fk = fk
     535        return formset
     536   
     537    def _resolve_foreign_key(cls, parent_model, model, fk_name=None):
     538        """
     539        Finds and returns the ForeignKey from model to parent if there is one.
     540        If fk_name is provided, assume it is the name of the ForeignKey field.
     541        """
     542        # avoid a circular import
     543        from django.db.models import ForeignKey
     544        opts = model._meta
     545        if fk_name:
     546            fks_to_parent = [f for f in opts.fields if f.name == fk_name]
     547            if len(fks_to_parent) == 1:
     548                fk = fks_to_parent[0]
     549                if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
     550                    raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
     551            elif len(fks_to_parent) == 0:
     552                raise Exception("%s has no field named '%s'" % (model, fk_name))
     553        else:
     554            # Try to discover what the ForeignKey from model to parent_model is
     555            fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
     556            if len(fks_to_parent) == 1:
     557                fk = fks_to_parent[0]
     558            elif len(fks_to_parent) == 0:
     559                raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
     560            else:
     561                raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
     562        return fk
     563    resolve_foreign_key = classmethod(_resolve_foreign_key)
    508564
    509 class InlineFormset(BaseModelFormSet):
     565class BaseInlineFormSet(BaseModelFormSet):
    510566    """A formset for child objects related to a parent."""
    511     def __init__(self, instance, data=None, files=None):
     567    def __init__(self, *args, **kwargs):
    512568        from django.db.models.fields.related import RelatedObject
    513         self.instance = instance
     569        opts = self._meta
     570        self.instance = kwargs.pop("instance", None)
    514571        # is there a better way to get the object descriptor?
    515         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    516         qs = self.get_queryset()
    517         super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)
     572        rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name()
     573        kwargs["prefix"] = rel_name
     574        super(BaseInlineFormSet, self).__init__(*args, **kwargs)
    518575
    519     def get_queryset(self):
     576    def get_queryset(self, **kwargs):
    520577        """
    521578        Returns this FormSet's queryset, but restricted to children of
    522579        self.instance
    523580        """
    524         kwargs = {self.fk.name: self.instance}
    525         return self.model._default_manager.filter(**kwargs)
     581        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     582        return queryset.filter(**{self.fk.name: self.instance})
    526583
    527584    def save_new(self, form, commit=True):
    528585        kwargs = {self.fk.get_attname(): self.instance.pk}
    529         new_obj = self.model(**kwargs)
     586        new_obj = self._meta.model(**kwargs)
    530587        return save_instance(form, new_obj, commit=commit)
    531588
    532 def get_foreign_key(parent_model, model, fk_name=None):
    533     """
    534     Finds and returns the ForeignKey from model to parent if there is one.
    535     If fk_name is provided, assume it is the name of the ForeignKey field.
    536     """
    537     # avoid circular import
    538     from django.db.models import ForeignKey
    539     opts = model._meta
    540     if fk_name:
    541         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    542         if len(fks_to_parent) == 1:
    543             fk = fks_to_parent[0]
    544             if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
    545                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    546         elif len(fks_to_parent) == 0:
    547             raise Exception("%s has no field named '%s'" % (model, fk_name))
    548     else:
    549         # Try to discover what the ForeignKey from model to parent_model is
    550         fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
    551         if len(fks_to_parent) == 1:
    552             fk = fks_to_parent[0]
    553         elif len(fks_to_parent) == 0:
    554             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    555         else:
    556             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    557     return fk
     589class InlineFormset(BaseInlineFormSet):
     590    __metaclass__ = InlineFormSetMetaclass
    558591
    559 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
     592def inline_formset(parent_model, model, formset=InlineFormset,
     593                   formfield_callback=lambda f: f.formfield(), **options):
    560594    """
    561595    Returns an ``InlineFormset`` for the given kwargs.
    562596
    563597    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    564598    to ``parent_model``.
    565599    """
    566     fk = get_foreign_key(parent_model, model, fk_name=fk_name)
    567     # let the formset handle object deletion by default
    568     FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
    569                                 formfield_callback=formfield_callback,
    570                                 extra=extra, orderable=orderable,
    571                                 deletable=deletable)
    572     # HACK: remove the ForeignKey to the parent from every form
    573     # This should be done a line above before we pass 'fields' to formset_for_model
    574     # an 'omit' argument would be very handy here
    575     try:
    576         del FormSet.form_class.base_fields[fk.name]
    577     except KeyError:
    578         pass
    579     FormSet.fk = fk
    580     return FormSet
     600    opts = model._meta
     601    options.update({"parent_model": parent_model, "model": model})
     602    return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,),
     603                                  {}, formfield_callback, **options)
  • new file django/newforms/options.py

    diff --git a/django/newforms/options.py b/django/newforms/options.py
    new file mode 100644
    index 0000000..f9696b8
    - +  
     1
     2from forms import BaseForm
     3
     4class BaseFormOptions(object):
     5    """
     6    The base class for all options that are associated to a form object.
     7    """
     8    def __init__(self, options=None):
     9        self.fields = self._dynamic_attribute(options, "fields")
     10        self.exclude = self._dynamic_attribute(options, "exclude")
     11   
     12    def _dynamic_attribute(self, obj, key, default=None):
     13        try:
     14            return getattr(obj, key)
     15        except AttributeError:
     16            try:
     17                return obj[key]
     18            except (TypeError, KeyError):
     19                # key doesnt exist in obj or obj is None
     20                return default
     21
     22class ModelFormOptions(BaseFormOptions):
     23    """
     24    Encapsulates the options on a ModelForm class.
     25    """
     26    def __init__(self, options=None):
     27        self.model = self._dynamic_attribute(options, "model")
     28        super(ModelFormOptions, self).__init__(options)
     29
     30class FormSetOptions(BaseFormOptions):
     31    """
     32    Encapsulates the options on a FormSet class.
     33    """
     34    def __init__(self, options=None):
     35        self.form = self._dynamic_attribute(options, "form", BaseForm)
     36        self.num_extra = self._dynamic_attribute(options, "num_extra", 1)
     37        self.orderable = self._dynamic_attribute(options, "orderable", False)
     38        self.deletable = self._dynamic_attribute(options, "deletable", False)
     39        super(FormSetOptions, self).__init__(options)
     40
     41class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
     42    def __init__(self, options=None):
     43        super(ModelFormSetOptions, self).__init__(options)
     44        self.deletable = True
     45
     46class InlineFormSetOptions(ModelFormSetOptions):
     47    def __init__(self, options=None):
     48        super(InlineFormSetOptions, self).__init__(options)
     49        self.parent_model = self._dynamic_attribute(options, "parent_model")
     50        self.fk_name = self._dynamic_attribute(options, "fk_name")
     51   
     52 No newline at end of file
  • tests/modeltests/model_forms/models.py

    diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
    index cd92956..5e95844 100644
    a b ImproperlyConfigured: BadForm's base classes define more than one model.  
    167167>>> class CategoryForm(ModelForm):
    168168...     class Meta:
    169169...         model = Category
    170 >>> f = CategoryForm(Category())
     170>>> f = CategoryForm()
    171171>>> print f
    172172<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr>
    173173<tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr>
    ImproperlyConfigured: BadForm's base classes define more than one model.  
    179179>>> print f['name']
    180180<input id="id_name" type="text" name="name" maxlength="20" />
    181181
    182 >>> f = CategoryForm(Category(), auto_id=False)
     182>>> f = CategoryForm(auto_id=False)
    183183>>> print f.as_ul()
    184184<li>Name: <input type="text" name="name" maxlength="20" /></li>
    185185<li>Slug: <input type="text" name="slug" maxlength="20" /></li>
    186186<li>The URL: <input type="text" name="url" maxlength="40" /></li>
    187187
    188 >>> f = CategoryForm(Category(), {'name': 'Entertainment', 'slug': 'entertainment', 'url': 'entertainment'})
     188>>> f = CategoryForm({'name': 'Entertainment', 'slug': 'entertainment', 'url': 'entertainment'})
    189189>>> f.is_valid()
    190190True
    191191>>> f.cleaned_data
    True  
    196196>>> Category.objects.all()
    197197[<Category: Entertainment>]
    198198
    199 >>> f = CategoryForm(Category(), {'name': "It's a test", 'slug': 'its-test', 'url': 'test'})
     199>>> f = CategoryForm({'name': "It's a test", 'slug': 'its-test', 'url': 'test'})
    200200>>> f.is_valid()
    201201True
    202202>>> f.cleaned_data
    True  
    210210If you call save() with commit=False, then it will return an object that
    211211hasn't yet been saved to the database. In this case, it's up to you to call
    212212save() on the resulting model instance.
    213 >>> f = CategoryForm(Category(), {'name': 'Third test', 'slug': 'third-test', 'url': 'third'})
     213>>> f = CategoryForm({'name': 'Third test', 'slug': 'third-test', 'url': 'third'})
    214214>>> f.is_valid()
    215215True
    216216>>> f.cleaned_data
    True  
    225225[<Category: Entertainment>, <Category: It's a test>, <Category: Third test>]
    226226
    227227If you call save() with invalid data, you'll get a ValueError.
    228 >>> f = CategoryForm(Category(), {'name': '', 'slug': '', 'url': 'foo'})
     228>>> f = CategoryForm({'name': '', 'slug': '', 'url': 'foo'})
    229229>>> f.errors
    230230{'name': [u'This field is required.'], 'slug': [u'This field is required.']}
    231231>>> f.cleaned_data
    AttributeError: 'CategoryForm' object has no attribute 'cleaned_data'  
    236236Traceback (most recent call last):
    237237...
    238238ValueError: The Category could not be created because the data didn't validate.
    239 >>> f = CategoryForm(Category(), {'name': '', 'slug': '', 'url': 'foo'})
     239>>> f = CategoryForm({'name': '', 'slug': '', 'url': 'foo'})
    240240>>> f.save()
    241241Traceback (most recent call last):
    242242...
    fields with the 'choices' attribute are represented by a ChoiceField.  
    253253>>> class ArticleForm(ModelForm):
    254254...     class Meta:
    255255...         model = Article
    256 >>> f = ArticleForm(Article(), auto_id=False)
     256>>> f = ArticleForm(auto_id=False)
    257257>>> print f
    258258<tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr>
    259259<tr><th>Slug:</th><td><input type="text" name="slug" maxlength="50" /></td></tr>
    from the form can't provide a value for that field!  
    286286...     class Meta:
    287287...         model = Article
    288288...         fields = ('headline','pub_date')
    289 >>> f = PartialArticleForm(Article(), auto_id=False)
     289>>> f = PartialArticleForm(auto_id=False)
    290290>>> print f
    291291<tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr>
    292292<tr><th>Pub date:</th><td><input type="text" name="pub_date" /></td></tr>
    current values are inserted as 'initial' data in each Field.  
    298298>>> class RoykoForm(ModelForm):
    299299...     class Meta:
    300300...         model = Writer
    301 >>> f = RoykoForm(w, auto_id=False)
     301>>> f = RoykoForm(auto_id=False, instance=w)
    302302>>> print f
    303303<tr><th>Name:</th><td><input type="text" name="name" value="Mike Royko" maxlength="50" /><br />Use both first and last names.</td></tr>
    304304
    current values are inserted as 'initial' data in each Field.  
    309309>>> class TestArticleForm(ModelForm):
    310310...     class Meta:
    311311...         model = Article
    312 >>> f = TestArticleForm(art, auto_id=False)
     312>>> f = TestArticleForm(auto_id=False, instance=art)
    313313>>> print f.as_ul()
    314314<li>Headline: <input type="text" name="headline" value="Test article" maxlength="50" /></li>
    315315<li>Slug: <input type="text" name="slug" value="test-article" maxlength="50" /></li>
    current values are inserted as 'initial' data in each Field.  
    331331<option value="2">It&#39;s a test</option>
    332332<option value="3">Third test</option>
    333333</select>  Hold down "Control", or "Command" on a Mac, to select more than one.</li>
    334 >>> f = TestArticleForm(art, {'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'})
     334>>> f = TestArticleForm({'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'}, instance=art)
    335335>>> f.is_valid()
    336336True
    337337>>> test_art = f.save()
    by specifying a 'fields' argument to form_for_instance.  
    347347...     class Meta:
    348348...         model = Article
    349349...         fields=('headline', 'slug', 'pub_date')
    350 >>> f = PartialArticleForm(art, {'headline': u'New headline', 'slug': 'new-headline', 'pub_date': u'1988-01-04'}, auto_id=False)
     350>>> f = PartialArticleForm({'headline': u'New headline', 'slug': 'new-headline', 'pub_date': u'1988-01-04'}, auto_id=False, instance=art)
    351351>>> print f.as_ul()
    352352<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li>
    353353<li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li>
    Add some categories and test the many-to-many form output.  
    370370>>> class TestArticleForm(ModelForm):
    371371...     class Meta:
    372372...         model = Article
    373 >>> f = TestArticleForm(new_art, auto_id=False)
     373>>> f = TestArticleForm(auto_id=False, instance=new_art)
    374374>>> print f.as_ul()
    375375<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li>
    376376<li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li>
    Add some categories and test the many-to-many form output.  
    393393<option value="3">Third test</option>
    394394</select>  Hold down "Control", or "Command" on a Mac, to select more than one.</li>
    395395
    396 >>> f = TestArticleForm(new_art, {'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',
    397 ...     'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']})
     396>>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',
     397...     'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']}, instance=new_art)
    398398>>> new_art = f.save()
    399399>>> new_art.id
    4004001
    Add some categories and test the many-to-many form output.  
    403403[<Category: Entertainment>, <Category: It's a test>]
    404404
    405405Now, submit form data with no categories. This deletes the existing categories.
    406 >>> f = TestArticleForm(new_art, {'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',
    407 ...     'writer': u'1', 'article': u'Hello.'})
     406>>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',
     407...     'writer': u'1', 'article': u'Hello.'}, instance=new_art)
    408408>>> new_art = f.save()
    409409>>> new_art.id
    4104101
    Create a new article, with categories, via the form.  
    416416>>> class ArticleForm(ModelForm):
    417417...     class Meta:
    418418...         model = Article
    419 >>> f = ArticleForm(Article(), {'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01',
     419>>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01',
    420420...     'writer': u'1', 'article': u'Test.', 'categories': [u'1', u'2']})
    421421>>> new_art = f.save()
    422422>>> new_art.id
    Create a new article, with no categories, via the form.  
    429429>>> class ArticleForm(ModelForm):
    430430...     class Meta:
    431431...         model = Article
    432 >>> f = ArticleForm(Article(), {'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01',
     432>>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01',
    433433...     'writer': u'1', 'article': u'Test.'})
    434434>>> new_art = f.save()
    435435>>> new_art.id
    The m2m data won't be saved until save_m2m() is invoked on the form.  
    443443>>> class ArticleForm(ModelForm):
    444444...     class Meta:
    445445...         model = Article
    446 >>> f = ArticleForm(Article(), {'headline': u'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': u'1967-11-01',
     446>>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': u'1967-11-01',
    447447...     'writer': u'1', 'article': u'Test.', 'categories': [u'1', u'2']})
    448448>>> new_art = f.save(commit=False)
    449449
    existing Category instance.  
    474474<Category: Third test>
    475475>>> cat.id
    4764763
    477 >>> form = ShortCategory(cat, {'name': 'Third', 'slug': 'third', 'url': '3rd'})
     477>>> form = ShortCategory({'name': 'Third', 'slug': 'third', 'url': '3rd'}, instance=cat)
    478478>>> form.save()
    479479<Category: Third>
    480480>>> Category.objects.get(id=3)
    the data in the database when the form is instantiated.  
    486486>>> class ArticleForm(ModelForm):
    487487...     class Meta:
    488488...         model = Article
    489 >>> f = ArticleForm(Article(), auto_id=False)
     489>>> f = ArticleForm(auto_id=False)
    490490>>> print f.as_ul()
    491491<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
    492492<li>Slug: <input type="text" name="slug" maxlength="50" /></li>
    ValidationError: [u'Select a valid choice. 4 is not one of the available choices  
    690690>>> class PhoneNumberForm(ModelForm):
    691691...     class Meta:
    692692...         model = PhoneNumber
    693 >>> f = PhoneNumberForm(PhoneNumber(), {'phone': '(312) 555-1212', 'description': 'Assistance'})
     693>>> f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'})
    694694>>> f.is_valid()
    695695True
    696696>>> f.cleaned_data
  • tests/modeltests/model_formsets/models.py

    diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
    index 19bdeed..d1e72ea 100644
    a b class Book(models.Model):  
    1616
    1717__test__ = {'API_TESTS': """
    1818
    19 >>> from django.newforms.models import formset_for_model
     19>>> from django import newforms as forms
     20>>> from django.newforms.models import formset_for_model, ModelFormSet
    2021
    21 >>> qs = Author.objects.all()
    22 >>> AuthorFormSet = formset_for_model(Author, extra=3)
     22A bare bones verion.
    2323
    24 >>> formset = AuthorFormSet(qs)
     24>>> class AuthorFormSet(ModelFormSet):
     25...     class Meta:
     26...         model = Author
     27>>> AuthorFormSet.base_fields.keys()
     28['name']
     29
     30Extra fields.
     31
     32>>> class AuthorFormSet(ModelFormSet):
     33...     published = forms.BooleanField()
     34...
     35...     class Meta:
     36...         model = Author
     37>>> AuthorFormSet.base_fields.keys()
     38['name', 'published']
     39
     40Lets create a formset that is bound to a model.
     41
     42>>> class AuthorFormSet(ModelFormSet):
     43...     class Meta:
     44...         model = Author
     45...         num_extra = 3
     46
     47>>> formset = AuthorFormSet()
    2548>>> for form in formset.forms:
    2649...     print form.as_p()
    2750<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>
    __test__ = {'API_TESTS': """  
    3558...     'form-2-name': '',
    3659... }
    3760
    38 >>> formset = AuthorFormSet(qs, data=data)
     61>>> formset = AuthorFormSet(data)
    3962>>> formset.is_valid()
    4063True
    4164
    Charles Baudelaire  
    4972
    5073
    5174Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
    52 authors with an extra form to add him. This time we'll use formset_for_queryset.
    53 We *could* use formset_for_queryset to restrict the Author objects we edit,
    54 but in that case we'll use it to display them in alphabetical order by name.
    55 
    56 >>> qs = Author.objects.order_by('name')
    57 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False)
    58 
    59 >>> formset = AuthorFormSet(qs)
     75authors with an extra form to add him. When subclassing ModelFormSet you can
     76override the get_queryset method to return any queryset we like, but in this
     77case we'll use it to display it in alphabetical order by name.
     78
     79>>> class AuthorFormSet(ModelFormSet):
     80...     class Meta:
     81...         model = Author
     82...         num_extra = 1
     83...         deletable = False
     84...
     85...     def get_queryset(self, **kwargs):
     86...         qs = super(AuthorFormSet, self).get_queryset(**kwargs)
     87...         return qs.order_by('name')
     88
     89>>> formset = AuthorFormSet()
    6090>>> for form in formset.forms:
    6191...     print form.as_p()
    6292<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>
    but in that case we'll use it to display them in alphabetical order by name.  
    73103...     'form-2-name': 'Paul Verlaine',
    74104... }
    75105
    76 >>> formset = AuthorFormSet(qs, data=data)
     106>>> formset = AuthorFormSet(data)
    77107>>> formset.is_valid()
    78108True
    79109
    Paul Verlaine  
    90120This probably shouldn't happen, but it will. If an add form was marked for
    91121deltetion, make sure we don't save that form.
    92122
    93 >>> qs = Author.objects.order_by('name')
    94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
    95 
    96 >>> formset = AuthorFormSet(qs)
     123>>> class AuthorFormSet(ModelFormSet):
     124...     class Meta:
     125...         model = Author
     126...         num_extra = 1
     127...         deletable = True
     128...
     129...     def get_queryset(self, **kwargs):
     130...         qs = super(AuthorFormSet, self).get_queryset(**kwargs)
     131...         return qs.order_by('name')
     132
     133>>> formset = AuthorFormSet()
    97134>>> for form in formset.forms:
    98135...     print form.as_p()
    99136<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>
    deltetion, make sure we don't save that form.  
    117154...     'form-3-DELETE': 'on',
    118155... }
    119156
    120 >>> formset = AuthorFormSet(qs, data=data)
     157>>> formset = AuthorFormSet(data)
    121158>>> formset.is_valid()
    122159True
    123160
    Paul Verlaine  
    134171We can also create a formset that is tied to a parent model. This is how the
    135172admin system's edit inline functionality works.
    136173
    137 >>> from django.newforms.models import inline_formset
     174>>> from django.newforms.models import inline_formset, InlineFormset
     175
     176>>> class AuthorBooksFormSet(InlineFormset):
     177...     class Meta:
     178...         parent_model = Author
     179...         model = Book
     180...         num_extra = 3
     181...         deletable = False
    138182
    139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
    140183>>> author = Author.objects.get(name='Charles Baudelaire')
    141184
    142 >>> formset = AuthorBooksFormSet(author)
     185>>> formset = AuthorBooksFormSet(instance=author)
    143186>>> for form in formset.forms:
    144187...     print form.as_p()
    145188<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>
    admin system's edit inline functionality works.  
    153196...     'book_set-2-title': '',
    154197... }
    155198
    156 >>> formset = AuthorBooksFormSet(author, data=data)
     199>>> formset = AuthorBooksFormSet(data, instance=author)
    157200>>> formset.is_valid()
    158201True
    159202
    Now that we've added a book to Charles Baudelaire, let's try adding another  
    169212one. This time though, an edit form will be available for every existing
    170213book.
    171214
    172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
     215>>> class AuthorBooksFormSet(InlineFormset):
     216...     class Meta:
     217...         parent_model = Author
     218...         model = Book
     219...         num_extra = 2
     220...         deletable = False
     221
    173222>>> author = Author.objects.get(name='Charles Baudelaire')
    174223
    175 >>> formset = AuthorBooksFormSet(author)
     224>>> formset = AuthorBooksFormSet(instance=author)
    176225>>> for form in formset.forms:
    177226...     print form.as_p()
    178227<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>
    book.  
    187236...     'book_set-2-title': '',
    188237... }
    189238
    190 >>> formset = AuthorBooksFormSet(author, data=data)
     239>>> formset = AuthorBooksFormSet(data, instance=author)
    191240>>> formset.is_valid()
    192241True
    193242
  • tests/regressiontests/forms/formsets.py

    diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py
    index a6da2fe..f5cb2ee 100644
    a b  
    22formset_tests = """
    33# Basic FormSet creation and usage ############################################
    44
    5 FormSet allows us to use multiple instance of the same form on 1 page. For now,
    6 the best way to create a FormSet is by using the formset_for_form function.
     5FormSet allows us to use multiple instance of the same form on 1 page. Create
     6the formset as you would a regular form by defining the fields declaratively.
    77
    88>>> from django.newforms import Form, CharField, IntegerField, ValidationError
    9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet
     9>>> from django.newforms import BooleanField
     10>>> from django.newforms.formsets import formset_for_form, BaseFormSet, FormSet
    1011
    11 >>> class Choice(Form):
     12>>> class ChoiceFormSet(FormSet):
    1213...     choice = CharField()
    1314...     votes = IntegerField()
    1415
    15 >>> ChoiceFormSet = formset_for_form(Choice)
    16 
    1716
    1817A FormSet constructor takes the same arguments as Form. Let's create a FormSet
    1918for adding data. By default, it displays 1 blank form. It can display more,
    False  
    145144>>> formset.errors
    146145[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}]
    147146
     147# Subclassing a FormSet class #################################################
     148
     149We can subclass a FormSet to add addition fields to an already exisiting
     150FormSet.
     151
     152>>> class SecondChoiceFormSet(ChoiceFormSet):
     153...     is_public = BooleanField()
     154
     155>>> formset = SecondChoiceFormSet(auto_id=False, prefix="choices")
     156>>> for form in formset.forms:
     157...     print form.as_ul()
     158<li>Choice: <input type="text" name="choices-0-choice" /></li>
     159<li>Votes: <input type="text" name="choices-0-votes" /></li>
     160<li>Is public: <input type="checkbox" name="choices-0-is_public" /></li>
    148161
    149162# Displaying more than 1 blank form ###########################################
    150163
    151 We can also display more than 1 empty form at a time. To do so, pass a
    152 num_extra argument to formset_for_form.
     164We can also display more than 1 empty form at a time. To do so, create an inner
     165Meta class with an attribute num_extra.
    153166
    154 >>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)
     167>>> class NumExtraChoiceFormSet(ChoiceFormSet):
     168...     class Meta:
     169...         num_extra = 3
    155170
    156 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
     171>>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices')
    157172>>> for form in formset.forms:
    158173...    print form.as_ul()
    159174<li>Choice: <input type="text" name="choices-0-choice" /></li>
    number of forms to be completed.  
    177192...     'choices-2-votes': '',
    178193... }
    179194
    180 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     195>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    181196>>> formset.is_valid()
    182197True
    183198>>> formset.cleaned_data
    We can just fill out one of the forms.  
    196211...     'choices-2-votes': '',
    197212... }
    198213
    199 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     214>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    200215>>> formset.is_valid()
    201216True
    202217>>> formset.cleaned_data
    And once again, if we try to partially complete a form, validation will fail.  
    215230...     'choices-2-votes': '',
    216231... }
    217232
    218 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     233>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    219234>>> formset.is_valid()
    220235False
    221236>>> formset.errors
    The num_extra argument also works when the formset is pre-filled with initial  
    226241data.
    227242
    228243>>> initial = [{'choice': u'Calexico', 'votes': 100}]
    229 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     244>>> formset = NumExtraChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    230245>>> for form in formset.forms:
    231246...    print form.as_ul()
    232247<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
    get an error.  
    254269...     'choices-3-votes': '',
    255270... }
    256271
    257 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     272>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    258273>>> formset.is_valid()
    259274False
    260275>>> formset.errors
    False  
    263278
    264279# FormSets with deletion ######################################################
    265280
    266 We can easily add deletion ability to a FormSet with an agrument to
    267 formset_for_form. This will add a boolean field to each form instance. When
    268 that boolean field is True, the cleaned data will be in formset.deleted_data
     281We can easily add deletion ability to a FormSet by setting deletable to True
     282in the inner Meta class. This will add a boolean field to each form instance.
     283When that boolean field is True, the cleaned data will be in formset.deleted_data
    269284rather than formset.cleaned_data
    270285
    271 >>> ChoiceFormSet = formset_for_form(Choice, deletable=True)
     286>>> class DeletableChoiceFormSet(ChoiceFormSet):
     287...     class Meta:
     288...         deletable = True
    272289
    273290>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
    274 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     291>>> formset = DeletableChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    275292>>> for form in formset.forms:
    276293...    print form.as_ul()
    277294<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
    To delete something, we just need to set that form's special delete field to  
    300317...     'choices-2-DELETE': '',
    301318... }
    302319
    303 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     320>>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices')
    304321>>> formset.is_valid()
    305322True
    306323>>> formset.cleaned_data
    True  
    310327
    311328# FormSets with ordering ######################################################
    312329
    313 We can also add ordering ability to a FormSet with an agrument to
    314 formset_for_form. This will add a integer field to each form instance. When
     330We can also add ordering ability to a FormSet by setting orderable to True in
     331the inner Meta class. This will add a integer field to each form instance. When
    315332form validation succeeds, formset.cleaned_data will have the data in the correct
    316333order specified by the ordering fields. If a number is duplicated in the set
    317334of ordering fields, for instance form 0 and form 3 are both marked as 1, then
    318335the form index used as a secondary ordering criteria. In order to put
    319336something at the front of the list, you'd need to set it's order to 0.
    320337
    321 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True)
     338>>> class OrderableChoiceFormSet(ChoiceFormSet):
     339...     class Meta:
     340...         orderable = True
    322341
    323342>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
    324 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     343>>> formset = OrderableChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    325344>>> for form in formset.forms:
    326345...    print form.as_ul()
    327346<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
    something at the front of the list, you'd need to set it's order to 0.  
    347366...     'choices-2-ORDER': '0',
    348367... }
    349368
    350 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     369>>> formset = OrderableChoiceFormSet(data, auto_id=False, prefix='choices')
    351370>>> formset.is_valid()
    352371True
    353372>>> for cleaned_data in formset.cleaned_data:
    True  
    359378# FormSets with ordering + deletion ###########################################
    360379
    361380Let's try throwing ordering and deletion into the same form.
     381TODO: Perhaps handle Meta class inheritance so you can subclass
     382OrderableChoiceFormSet and DeletableChoiceFormSet?
    362383
    363 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)
     384>>> class MixedChoiceFormSet(ChoiceFormSet):
     385...     class Meta:
     386...         orderable = True
     387...         deletable = True
    364388
    365389>>> initial = [
    366390...     {'choice': u'Calexico', 'votes': 100},
    367391...     {'choice': u'Fergie', 'votes': 900},
    368392...     {'choice': u'The Decemberists', 'votes': 500},
    369393... ]
    370 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     394>>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    371395>>> for form in formset.forms:
    372396...    print form.as_ul()
    373397<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
    Let's delete Fergie, and put The Decemberists ahead of Calexico.  
    409433...     'choices-3-DELETE': '',
    410434... }
    411435
    412 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     436>>> formset = MixedChoiceFormSet(data, auto_id=False, prefix='choices')
    413437>>> formset.is_valid()
    414438True
    415439>>> for cleaned_data in formset.cleaned_data:
    particular form. It follows the same pattern as the clean hook on Forms.  
    428452Let's define a FormSet that takes a list of favorite drinks, but raises am
    429453error if there are any duplicates.
    430454
    431 >>> class FavoriteDrinkForm(Form):
     455>>> class FavoriteDrinksFormSet(FormSet):
    432456...     name = CharField()
    433 ...
    434 
    435 >>> class FavoriteDrinksFormSet(BaseFormSet):
    436 ...     form_class = FavoriteDrinkForm
    437 ...     num_extra = 2
    438 ...     orderable = False
    439 ...     deletable = False
     457...     
     458...     class Meta:
     459...         num_extra = 2
     460...         orderable = False
     461...         deletable = False
    440462...
    441463...     def clean(self):
    442464...         seen_drinks = []
  • tests/regressiontests/inline_formsets/models.py

    diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py
    index f84be84..180bba1 100644
    a b __test__ = {'API_TESTS': """  
    2121Child has two ForeignKeys to Parent, so if we don't specify which one to use
    2222for the inline formset, we should get an exception.
    2323
    24 >>> ifs = inline_formset(Parent, Child)
     24>>> inline_formset(Parent, Child)()
    2525Traceback (most recent call last):
    2626    ...
    2727Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
    Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than  
    2929
    3030These two should both work without a problem.
    3131
    32 >>> ifs = inline_formset(Parent, Child, fk_name='mother')
    33 >>> ifs = inline_formset(Parent, Child, fk_name='father')
     32>>> ifs = inline_formset(Parent, Child, fk_name='mother')()
     33>>> ifs = inline_formset(Parent, Child, fk_name='father')()
    3434
    3535
    3636If we specify fk_name, but it isn't a ForeignKey from the child model to the
    3737parent model, we should get an exception.
    3838
    39 >>> ifs = inline_formset(Parent, Child, fk_name='school')
     39>>> inline_formset(Parent, Child, fk_name='school')()
    4040Traceback (most recent call last):
    4141    ...
    4242Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
    Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inlin  
    4545If the field specified in fk_name is not a ForeignKey, we should get an
    4646exception.
    4747
    48 >>> ifs = inline_formset(Parent, Child, fk_name='test')
     48>>> inline_formset(Parent, Child, fk_name='test')()
    4949Traceback (most recent call last):
    5050    ...
    5151Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
Back to Top