Ticket #6241: formset_refactor_9.diff

File formset_refactor_9.diff, 52.0 KB (added by Brian Rosner, 16 years ago)

includes fixes recommended by malcolm, joseph and karen. also some various cleanups.

  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index c3289ea..ee28cb4 100644
    a b  
    11from django import oldforms, template
    22from django import newforms as forms
    33from django.newforms.formsets import all_valid
     4from django.newforms.models import modelform_for_model, inline_formset
    45from django.contrib.contenttypes.models import ContentType
    56from django.contrib.admin import widgets
    67from django.contrib.admin.util import get_deleted_objects
    class ModelAdmin(BaseModelAdmin):  
    342343            fields = flatten_fieldsets(self.declared_fieldsets)
    343344        else:
    344345            fields = None
    345         return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
     346        return modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    346347
    347348    def form_change(self, request, obj):
    348349        """
    class ModelAdmin(BaseModelAdmin):  
    352353            fields = flatten_fieldsets(self.declared_fieldsets)
    353354        else:
    354355            fields = None
    355         return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)
     356        return modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    356357
    357358    def save_add(self, request, model, form, formsets, post_url_continue):
    358359        """
    class ModelAdmin(BaseModelAdmin):  
    492493            # Object list will give 'Permission Denied', so go back to admin home
    493494            post_url = '../../../'
    494495
    495         ModelForm = self.form_add(request)
     496        Form = self.form_add(request)
    496497        inline_formsets = []
    497498        obj = self.model()
    498499        if request.method == 'POST':
    499             form = ModelForm(request.POST, request.FILES)
     500            form = Form(request.POST, request.FILES)
    500501            for FormSet in self.formsets_add(request):
    501                 inline_formset = FormSet(obj, data=request.POST, files=request.FILES)
     502                inline_formset = FormSet(request.POST, request.FILES)
    502503                inline_formsets.append(inline_formset)
    503504            if all_valid(inline_formsets) and form.is_valid():
    504505                return self.save_add(request, model, form, inline_formsets, '../%s/')
    505506        else:
    506             form = ModelForm(initial=request.GET)
     507            form = Form(initial=request.GET)
    507508            for FormSet in self.formsets_add(request):
    508                 inline_formset = FormSet(obj)
     509                inline_formset = FormSet()
    509510                inline_formsets.append(inline_formset)
    510511
    511512        adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
    class ModelAdmin(BaseModelAdmin):  
    552553        if request.POST and request.POST.has_key("_saveasnew"):
    553554            return self.add_view(request, form_url='../../add/')
    554555
    555         ModelForm = self.form_change(request, obj)
     556        Form = self.form_change(request, obj)
    556557        inline_formsets = []
    557558        if request.method == 'POST':
    558             form = ModelForm(request.POST, request.FILES)
     559            form = Form(request.POST, request.FILES, instance=obj)
    559560            for FormSet in self.formsets_change(request, obj):
    560                 inline_formset = FormSet(obj, request.POST, request.FILES)
     561                inline_formset = FormSet(request.POST, request.FILES, instance=obj)
    561562                inline_formsets.append(inline_formset)
    562563
    563564            if all_valid(inline_formsets) and form.is_valid():
    564565                return self.save_change(request, model, form, inline_formsets)
    565566        else:
    566             form = ModelForm()
     567            form = Form(instance=obj, initial=request.GET)
    567568            for FormSet in self.formsets_change(request, obj):
    568                 inline_formset = FormSet(obj)
     569                inline_formset = FormSet(instance=obj)
    569570                inline_formsets.append(inline_formset)
    570571
    571572        ## Populate the FormWrapper.
    class InlineModelAdmin(BaseModelAdmin):  
    742743            fields = flatten_fieldsets(self.declared_fieldsets)
    743744        else:
    744745            fields = None
    745         return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
     746        return inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
    746747
    747748    def formset_change(self, request, obj):
    748749        """Returns an InlineFormSet class for use in admin change views."""
    class InlineModelAdmin(BaseModelAdmin):  
    750751            fields = flatten_fieldsets(self.declared_fieldsets)
    751752        else:
    752753            fields = None
    753         return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
     754        return inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra)
    754755
    755756    def fieldsets_add(self, request):
    756757        if self.declared_fieldsets:
    757758            return self.declared_fieldsets
    758         form = self.formset_add(request).form_class
    759         return [(None, {'fields': form.base_fields.keys()})]
     759        formset = self.formset_add(request)
     760        return [(None, {'fields': formset.base_fields.keys()})]
    760761
    761762    def fieldsets_change(self, request, obj):
    762763        if self.declared_fieldsets:
    763764            return self.declared_fieldsets
    764         form = self.formset_change(request, obj).form_class
    765         return [(None, {'fields': form.base_fields.keys()})]
     765        formset = self.formset_change(request, obj)
     766        return [(None, {'fields': formset.base_fields.keys()})]
    766767
    767768class StackedInline(InlineModelAdmin):
    768769    template = 'admin/edit_inline/stacked.html'
    class InlineAdminFormSet(object):  
    778779        self.opts = inline
    779780        self.formset = formset
    780781        self.fieldsets = fieldsets
     782        # place orderable and deletable here since _meta is inaccesible in the
     783        # templates.
     784        self.orderable = formset._meta.orderable
     785        self.deletable = formset._meta.deletable
    781786
    782787    def __iter__(self):
    783788        for form, original in zip(self.formset.change_forms, self.formset.get_queryset()):
    class InlineAdminFormSet(object):  
    787792
    788793    def fields(self):
    789794        for field_name in flatten_fieldsets(self.fieldsets):
    790             yield self.formset.form_class.base_fields[field_name]
     795            yield self.formset.base_fields[field_name]
    791796
    792797class InlineAdminForm(AdminForm):
    793798    """
  • 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 e0f2cde..ecf983f 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',
    22     'formset_for_model', 'inline_formset',
     23    'ModelFormSet', 'InlineFormset',
    2324    'ModelChoiceField', 'ModelMultipleChoiceField',
    2425)
    2526
    def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda  
    207208            field_list.append((f.name, formfield))
    208209    return SortedDict(field_list)
    209210
    210 class ModelFormOptions(object):
    211     def __init__(self, options=None):
    212         self.model = getattr(options, 'model', None)
    213         self.fields = getattr(options, 'fields', None)
    214         self.exclude = getattr(options, 'exclude', None)
    215 
    216211class ModelFormMetaclass(type):
     212    opts_class = ModelFormOptions
     213   
    217214    def __new__(cls, name, bases, attrs,
    218                 formfield_callback=lambda f: f.formfield()):
     215                formfield_callback=lambda f: f.formfield(), **options):
    219216        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
    220217        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
    221218
    class ModelFormMetaclass(type):  
    227224                fields = base.base_fields.items() + fields
    228225        declared_fields = SortedDict(fields)
    229226
    230         opts = ModelFormOptions(attrs.get('Meta', None))
     227        opts = cls.opts_class(options and options or attrs.get('Meta', None))
    231228        attrs['_meta'] = opts
    232229
    233230        # Don't allow more than one Meta model defenition in bases. The fields
    class ModelFormMetaclass(type):  
    260257        else:
    261258            attrs['base_fields'] = declared_fields
    262259        return type.__new__(cls, name, bases, attrs)
    263 
     260       
    264261class BaseModelForm(BaseForm):
    265262    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    266263                 initial=None, error_class=ErrorList, label_suffix=':', instance=None):
    class BaseModelForm(BaseForm):  
    293290class ModelForm(BaseModelForm):
    294291    __metaclass__ = ModelFormMetaclass
    295292
     293def modelform_for_model(model, form=ModelForm,
     294                        formfield_callback=lambda f: f.formfield(), **options):
     295    opts = model._meta
     296    options.update({"model": model})
     297    return ModelFormMetaclass(opts.object_name + "ModelForm", (form,),
     298                              {}, formfield_callback, **options)
     299
    296300
    297301# Fields #####################################################################
    298302
    class ModelMultipleChoiceField(ModelChoiceField):  
    407411
    408412# Model-FormSet integration ###################################################
    409413
    410 def initial_data(instance, fields=None):
    411     """
    412     Return a dictionary from data in ``instance`` that is suitable for
    413     use as a ``Form`` constructor's ``initial`` argument.
    414 
    415     Provide ``fields`` to specify the names of specific fields to return.
    416     All field values in the instance will be returned if ``fields`` is not
    417     provided.
    418     """
    419     # avoid a circular import
    420     from django.db.models.fields.related import ManyToManyField
    421     opts = instance._meta
    422     initial = {}
    423     for f in opts.fields + opts.many_to_many:
    424         if not f.editable:
    425             continue
    426         if fields and not f.name in fields:
    427             continue
    428         if isinstance(f, ManyToManyField):
    429             # MultipleChoiceWidget needs a list of ints, not object instances.
    430             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
    431         else:
    432             initial[f.name] = f.value_from_object(instance)
    433     return initial
     414class ModelFormSetMetaclass(ModelFormMetaclass):
     415    opts_class = ModelFormSetOptions
    434416
    435417class BaseModelFormSet(BaseFormSet):
    436418    """
    437419    A ``FormSet`` for editing a queryset and/or adding new objects to it.
    438420    """
    439     model = None
    440     queryset = None
    441421
    442     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):
     422    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
     423                 queryset=None):
    443424        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
    444         self.queryset = qs
    445         kwargs['initial'] = [initial_data(obj) for obj in qs]
     425        opts = self._meta
     426        if queryset is None:
     427            self.queryset = self.get_queryset(**kwargs)
     428        else:
     429            self.queryset = queryset
     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
    446434        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()
    447441
    448442    def save_new(self, form, commit=True):
    449443        """Saves and returns a new model instance for the given form."""
    450         return save_instance(form, self.model(), commit=commit)
     444        return save_instance(form, self._meta.model(), commit=commit)
    451445
    452446    def save_instance(self, form, instance, commit=True):
    453447        """Saves and returns an existing model instance for the given form."""
    class BaseModelFormSet(BaseFormSet):  
    462456    def save_existing_objects(self, commit=True):
    463457        if not self.queryset:
    464458            return []
    465         # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
     459        # Put the objects from self.queryset into a dict so they are easy to lookup by pk
    466460        existing_objects = {}
     461        opts = self._meta
    467462        for obj in self.queryset:
    468463            existing_objects[obj.pk] = obj
    469464        saved_instances = []
    470465        for form in self.change_forms:
    471             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
    472             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]:
    473468                obj.delete()
    474469            else:
    475470                saved_instances.append(self.save_instance(form, obj, commit=commit))
    class BaseModelFormSet(BaseFormSet):  
    477472
    478473    def save_new_objects(self, commit=True):
    479474        new_objects = []
     475        opts = self._meta
    480476        for form in self.add_forms:
    481477            if form.is_empty():
    482478                continue
    483479            # If someone has marked an add form for deletion, don't save the
    484480            # object. At some point it would be nice if we didn't display
    485481            # the deletion widget for add forms.
    486             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     482            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    487483                continue
    488484            new_objects.append(self.save_new(form, commit=commit))
    489485        return new_objects
    490486
    491487    def add_fields(self, form, index):
    492488        """Add a hidden field for the object's primary key."""
    493         self._pk_field_name = self.model._meta.pk.attname
     489        self._pk_field_name = self._meta.model._meta.pk.attname
    494490        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    495491        super(BaseModelFormSet, self).add_fields(form, index)
    496492
    497 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
    498                       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):
    499498    """
    500499    Returns a FormSet class for the given Django model class. This FormSet
    501500    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  
    504503    This is essentially the same as ``formset_for_queryset``, but automatically
    505504    uses the model's default manager to determine the queryset.
    506505    """
    507     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
    508     FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    509     FormSet.model = model
    510     return FormSet
     506    opts = model._meta
     507    options.update({"model": model})
     508    return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,),
     509                                 {}, formfield_callback, **options)
    511510
    512 class InlineFormset(BaseModelFormSet):
     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)
     564
     565class BaseInlineFormSet(BaseModelFormSet):
    513566    """A formset for child objects related to a parent."""
    514     def __init__(self, instance, data=None, files=None):
     567    def __init__(self, *args, **kwargs):
    515568        from django.db.models.fields.related import RelatedObject
    516         self.instance = instance
     569        opts = self._meta
     570        self.instance = kwargs.pop("instance", None)
    517571        # is there a better way to get the object descriptor?
    518         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    519         qs = self.get_queryset()
    520         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)
    521575
    522     def get_queryset(self):
     576    def get_queryset(self, **kwargs):
    523577        """
    524578        Returns this FormSet's queryset, but restricted to children of
    525579        self.instance
    526580        """
    527         kwargs = {self.fk.name: self.instance}
    528         return self.model._default_manager.filter(**kwargs)
     581        opts = self._meta
     582        if self.instance is None:
     583            return opts.model._default_manager.none()
     584        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     585        return queryset.filter(**{self.fk.name: self.instance})
    529586
    530587    def save_new(self, form, commit=True):
    531588        kwargs = {self.fk.get_attname(): self.instance.pk}
    532         new_obj = self.model(**kwargs)
     589        new_obj = self._meta.model(**kwargs)
    533590        return save_instance(form, new_obj, commit=commit)
    534591
    535 def get_foreign_key(parent_model, model, fk_name=None):
    536     """
    537     Finds and returns the ForeignKey from model to parent if there is one.
    538     If fk_name is provided, assume it is the name of the ForeignKey field.
    539     """
    540     # avoid circular import
    541     from django.db.models import ForeignKey
    542     opts = model._meta
    543     if fk_name:
    544         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    545         if len(fks_to_parent) == 1:
    546             fk = fks_to_parent[0]
    547             if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
    548                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    549         elif len(fks_to_parent) == 0:
    550             raise Exception("%s has no field named '%s'" % (model, fk_name))
    551     else:
    552         # Try to discover what the ForeignKey from model to parent_model is
    553         fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
    554         if len(fks_to_parent) == 1:
    555             fk = fks_to_parent[0]
    556         elif len(fks_to_parent) == 0:
    557             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    558         else:
    559             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    560     return fk
     592class InlineFormset(BaseInlineFormSet):
     593    __metaclass__ = InlineFormSetMetaclass
    561594
    562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
     595def inline_formset(parent_model, model, formset=InlineFormset,
     596                   formfield_callback=lambda f: f.formfield(), **options):
    563597    """
    564598    Returns an ``InlineFormset`` for the given kwargs.
    565599
    566600    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    567601    to ``parent_model``.
    568602    """
    569     fk = get_foreign_key(parent_model, model, fk_name=fk_name)
    570     # let the formset handle object deletion by default
    571     FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
    572                                 formfield_callback=formfield_callback,
    573                                 extra=extra, orderable=orderable,
    574                                 deletable=deletable)
    575     # HACK: remove the ForeignKey to the parent from every form
    576     # This should be done a line above before we pass 'fields' to formset_for_model
    577     # an 'omit' argument would be very handy here
    578     try:
    579         del FormSet.form_class.base_fields[fk.name]
    580     except KeyError:
    581         pass
    582     FormSet.fk = fk
    583     return FormSet
     603    opts = model._meta
     604    options.update({"parent_model": parent_model, "model": model})
     605    return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,),
     606                                  {}, 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..f8482fb
    - +  
     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    pass
     43
     44class InlineFormSetOptions(ModelFormSetOptions):
     45    def __init__(self, options=None):
     46        super(InlineFormSetOptions, self).__init__(options)
     47        self.parent_model = self._dynamic_attribute(options, "parent_model")
     48        self.num_extra = self._dynamic_attribute(options, "num_extra", 3)
     49        self.fk_name = self._dynamic_attribute(options, "fk_name")
     50        self.deletable = self._dynamic_attribute(options, "deletable", True)
     51   
     52 No newline at end of file
  • tests/modeltests/model_formsets/models.py

    diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
    index 19bdeed..cd16046 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)
     22Lets test the most basic use case of a ModelFormSet. This basically says give
     23me a FormSet that edits all the objects found in the Author model.
    2324
    24 >>> formset = AuthorFormSet(qs)
     25>>> class AuthorFormSet(ModelFormSet):
     26...     class Meta:
     27...         model = Author
     28>>> AuthorFormSet.base_fields.keys()
     29['name']
     30
     31Lets add on an extra field.
     32
     33>>> class AuthorFormSet(ModelFormSet):
     34...     published = forms.BooleanField()
     35...
     36...     class Meta:
     37...         model = Author
     38>>> AuthorFormSet.base_fields.keys()
     39['name', 'published']
     40
     41Lets create a formset that is bound to a model.
     42
     43>>> class AuthorFormSet(ModelFormSet):
     44...     class Meta:
     45...         model = Author
     46...         num_extra = 3
     47
     48>>> formset = AuthorFormSet()
    2549>>> for form in formset.forms:
    2650...     print form.as_p()
    2751<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': """  
    3559...     'form-2-name': '',
    3660... }
    3761
    38 >>> formset = AuthorFormSet(qs, data=data)
     62>>> formset = AuthorFormSet(data)
    3963>>> formset.is_valid()
    4064True
    4165
    Charles Baudelaire  
    4973
    5074
    5175Gah! 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)
     76authors with an extra form to add him. When subclassing ModelFormSet you can
     77override the get_queryset method to return any queryset we like, but in this
     78case we'll use it to display it in alphabetical order by name.
     79
     80>>> class AuthorFormSet(ModelFormSet):
     81...     class Meta:
     82...         model = Author
     83...         num_extra = 1
     84...         deletable = False
     85...
     86...     def get_queryset(self, **kwargs):
     87...         queryset = super(AuthorFormSet, self).get_queryset(**kwargs)
     88...         return queryset.order_by('name')
     89
     90>>> formset = AuthorFormSet()
    6091>>> for form in formset.forms:
    6192...     print form.as_p()
    6293<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.  
    73104...     'form-2-name': 'Paul Verlaine',
    74105... }
    75106
    76 >>> formset = AuthorFormSet(qs, data=data)
     107>>> formset = AuthorFormSet(data)
    77108>>> formset.is_valid()
    78109True
    79110
    Paul Verlaine  
    90121This probably shouldn't happen, but it will. If an add form was marked for
    91122deltetion, make sure we don't save that form.
    92123
    93 >>> qs = Author.objects.order_by('name')
    94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
    95 
    96 >>> formset = AuthorFormSet(qs)
     124>>> class AuthorFormSet(ModelFormSet):
     125...     class Meta:
     126...         model = Author
     127...         num_extra = 1
     128...         deletable = True
     129...
     130...     def get_queryset(self, **kwargs):
     131...         queryset = super(AuthorFormSet, self).get_queryset(**kwargs)
     132...         return queryset.order_by('name')
     133
     134>>> formset = AuthorFormSet()
    97135>>> for form in formset.forms:
    98136...     print form.as_p()
    99137<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.  
    117155...     'form-3-DELETE': 'on',
    118156... }
    119157
    120 >>> formset = AuthorFormSet(qs, data=data)
     158>>> formset = AuthorFormSet(data)
    121159>>> formset.is_valid()
    122160True
    123161
    Paul Verlaine  
    134172We can also create a formset that is tied to a parent model. This is how the
    135173admin system's edit inline functionality works.
    136174
    137 >>> from django.newforms.models import inline_formset
     175>>> from django.newforms.models import inline_formset, InlineFormset
     176
     177>>> class AuthorBooksFormSet(InlineFormset):
     178...     class Meta:
     179...         parent_model = Author
     180...         model = Book
     181...         num_extra = 3
     182...         deletable = False
    138183
    139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
    140184>>> author = Author.objects.get(name='Charles Baudelaire')
    141185
    142 >>> formset = AuthorBooksFormSet(author)
     186>>> formset = AuthorBooksFormSet(instance=author)
    143187>>> for form in formset.forms:
    144188...     print form.as_p()
    145189<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.  
    153197...     'book_set-2-title': '',
    154198... }
    155199
    156 >>> formset = AuthorBooksFormSet(author, data=data)
     200>>> formset = AuthorBooksFormSet(data, instance=author)
    157201>>> formset.is_valid()
    158202True
    159203
    Now that we've added a book to Charles Baudelaire, let's try adding another  
    169213one. This time though, an edit form will be available for every existing
    170214book.
    171215
    172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
     216>>> class AuthorBooksFormSet(InlineFormset):
     217...     class Meta:
     218...         parent_model = Author
     219...         model = Book
     220...         num_extra = 2
     221...         deletable = False
     222
    173223>>> author = Author.objects.get(name='Charles Baudelaire')
    174224
    175 >>> formset = AuthorBooksFormSet(author)
     225>>> formset = AuthorBooksFormSet(instance=author)
    176226>>> for form in formset.forms:
    177227...     print form.as_p()
    178228<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.  
    187237...     'book_set-2-title': '',
    188238... }
    189239
    190 >>> formset = AuthorBooksFormSet(author, data=data)
     240>>> formset = AuthorBooksFormSet(data, instance=author)
    191241>>> formset.is_valid()
    192242True
    193243
  • 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..0e89441 100644
    a b class Child(models.Model):  
    1515
    1616__test__ = {'API_TESTS': """
    1717
    18 >>> from django.newforms.models import inline_formset
    19 
     18>>> from django.newforms.models import InlineFormset
    2019
    2120Child has two ForeignKeys to Parent, so if we don't specify which one to use
    2221for the inline formset, we should get an exception.
    2322
    24 >>> ifs = inline_formset(Parent, Child)
     23>>> class ChildrenFormSet(InlineFormset):
     24...     class Meta:
     25...         parent_model = Parent
     26...         model = Child
    2527Traceback (most recent call last):
    2628    ...
    2729Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
    2830
    29 
    3031These two should both work without a problem.
    3132
    32 >>> ifs = inline_formset(Parent, Child, fk_name='mother')
    33 >>> ifs = inline_formset(Parent, Child, fk_name='father')
     33>>> class ChildrenFormSet(InlineFormset):
     34...     class Meta:
     35...         parent_model = Parent
     36...         model = Child
     37...         fk_name = "mother"
    3438
     39>>> class ChildrenFormSet(InlineFormset):
     40...     class Meta:
     41...         parent_model = Parent
     42...         model = Child
     43...         fk_name = "father"
    3544
    3645If we specify fk_name, but it isn't a ForeignKey from the child model to the
    3746parent model, we should get an exception.
    3847
    39 >>> ifs = inline_formset(Parent, Child, fk_name='school')
     48>>> class ChildrenFormSet(InlineFormset):
     49...     class Meta:
     50...         parent_model = Parent
     51...         model = Child
     52...         fk_name = "school"
    4053Traceback (most recent call last):
    4154    ...
    4255Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
    4356
    44 
    4557If the field specified in fk_name is not a ForeignKey, we should get an
    4658exception.
    4759
    48 >>> ifs = inline_formset(Parent, Child, fk_name='test')
     60>>> class ChildreFormSet(InlineFormset):
     61...     class Meta:
     62...         parent_model = Parent
     63...         model = Child
     64...         fk_name = "test"
    4965Traceback (most recent call last):
    5066    ...
    5167Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
Back to Top