Ticket #6241: formset_refactor_7125.diff

File formset_refactor_7125.diff, 49.5 KB (added by jkocherhans, 8 years ago)

Updated the patch to work with r7125. Still a couple of changes I'd like to make, but this is as far as I could get tonight.

  • 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..0360dd9 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, get_declared_fields
     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 FormSetMetaclass(type):
     31    def __new__(cls, name, bases, attrs, **options):
     32        attrs["base_fields"] = get_declared_fields(bases, attrs)
     33        attrs["_meta"] = FormSetOptions(options and options or attrs.get("Meta", None))
     34        return type.__new__(cls, name, bases, attrs)
     35
    2336class BaseFormSet(object):
    2437    """A collection of instances of the same Form class."""
    2538
    class BaseFormSet(object): 
    3750            self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
    3851            if self.management_form.is_valid():
    3952                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
     53                self.required_forms = self.total_forms - self._meta.num_extra
     54                self.change_form_count = self.total_forms - self._meta.num_extra
    4255            else:
    4356                # not sure that ValidationError is the best thing to raise here
    4457                raise ValidationError('ManagementForm data is missing or has been tampered with')
    4558        elif initial:
    4659            self.change_form_count = len(initial)
    4760            self.required_forms = len(initial)
    48             self.total_forms = self.required_forms + self.num_extra
     61            self.total_forms = self.required_forms + self._meta.num_extra
    4962            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
    5063        else:
    5164            self.change_form_count = 0
    5265            self.required_forms = 0
    53             self.total_forms = self.num_extra
     66            self.total_forms = self._meta.num_extra
    5467            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
    5568
    5669    def _get_add_forms(self):
    5770        """Return a list of all the add forms in this ``FormSet``."""
    58         FormClass = self.form_class
    5971        if not hasattr(self, '_add_forms'):
    6072            add_forms = []
    6173            for i in range(self.change_form_count, self.total_forms):
    class BaseFormSet(object): 
    6476                    kwargs['data'] = self.data
    6577                if self.files:
    6678                    kwargs['files'] = self.files
    67                 add_form = FormClass(**kwargs)
     79                add_form = self.get_form_class()(**kwargs)
    6880                self.add_fields(add_form, i)
    6981                add_forms.append(add_form)
    7082            self._add_forms = add_forms
    class BaseFormSet(object): 
    7385
    7486    def _get_change_forms(self):
    7587        """Return a list of all the change forms in this ``FormSet``."""
    76         FormClass = self.form_class
    7788        if not hasattr(self, '_change_forms'):
    7889            change_forms = []
    7990            for i in range(0, self.change_form_count):
    class BaseFormSet(object): 
    8495                    kwargs['files'] = self.files
    8596                if self.initial:
    8697                    kwargs['initial'] = self.initial[i]
    87                 change_form = FormClass(**kwargs)
     98                change_form = self.get_form_class(change=True)(**kwargs)
    8899                self.add_fields(change_form, i)
    89100                change_forms.append(change_form)
    90             self._change_forms= change_forms
     101            self._change_forms = change_forms
    91102        return self._change_forms
    92103    change_forms = property(_get_change_forms)
    93104
    class BaseFormSet(object): 
    117128        # Process change forms
    118129        for form in self.change_forms:
    119130            if form.is_valid():
    120                 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     131                if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    121132                    self.deleted_data.append(form.cleaned_data)
    122133                else:
    123134                    self.cleaned_data.append(form.cleaned_data)
    class BaseFormSet(object): 
    144155        add_errors.reverse()
    145156        errors.extend(add_errors)
    146157        # Sort cleaned_data if the formset is orderable.
    147         if self.orderable:
     158        if self._meta.orderable:
    148159            self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
    149160        # Give self.clean() a chance to do validation
    150161        try:
    class BaseFormSet(object): 
    167178        via formset.non_form_errors()
    168179        """
    169180        return self.cleaned_data
     181   
     182    def get_form_class(self, change=False):
     183        """
     184        A hook to change a form class object.
     185        """
     186        FormClass = self._meta.form
     187        FormClass.base_fields = self.base_fields
     188        return FormClass
    170189
    171190    def add_fields(self, form, index):
    172191        """A hook for adding extra fields on to each form instance."""
    173         if self.orderable:
     192        if self._meta.orderable:
    174193            form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
    175         if self.deletable:
     194        if self._meta.deletable:
    176195            form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
    177196
    178197    def add_prefix(self, index):
    class BaseFormSet(object): 
    192211        else:
    193212            return Media()
    194213    media = property(_get_media)
    195    
    196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
    197     """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)
    200214
     215class FormSet(BaseFormSet):
     216    __metaclass__ = FormSetMetaclass
     217
     218def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False,
     219                     deletable=False):
     220    """Return a FormSet for the given form class."""
     221    warn("formset_for_form is deprecated, use FormSet instead.",
     222         PendingDeprecationWarning,
     223         stacklevel=3)
     224    return BaseFormSetMetaclass(
     225        form.__name__ + "FormSet", (formset,), form.base_fields,
     226        form=form, num_extra=num_extra, orderable=orderable,
     227        deletable=deletable)
     228       
    201229def all_valid(formsets):
    202230    """Returns true if every formset in formsets is valid."""
    203231    valid = True
  • django/newforms/models.py

    diff --git a/django/newforms/models.py b/django/newforms/models.py
    index 3d71ee6..656c844 100644
    a b from django.core.exceptions import ImproperlyConfigured 
    1313from util import ValidationError, ErrorList
    1414from forms import BaseForm, get_declared_fields
    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 
    216 
    217211class ModelFormMetaclass(type):
     212    opts_class = ModelFormOptions
     213
    218214    def __new__(cls, name, bases, attrs,
    219                 formfield_callback=lambda f: f.formfield()):
     215                formfield_callback=lambda f: f.formfield(), **options):
    220216        try:
    221             parents = [b for b in bases if issubclass(b, ModelForm)]
     217            # XXX: The 3 issubclass checks here works, but calling this fragile
     218            # is being terribly nice. ModelFormSet and InlineFormSet could
     219            # possiblly inherit from ModelForm, but that seems wrong. More
     220            # thought needed.
     221            parents = [b for b in bases if issubclass(b, ModelForm) or issubclass(b, ModelFormSet) or issubclass(b, InlineFormSet)]
    222222        except NameError:
    223223            # We are defining ModelForm itself.
    224224            parents = None
    class ModelFormMetaclass(type): 
    228228
    229229        new_class = type.__new__(cls, name, bases, attrs)
    230230        declared_fields = get_declared_fields(bases, attrs, False)
    231         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
     231        opts = new_class._meta = cls.opts_class(options and options or getattr(new_class, 'Meta', None))
    232232        if opts.model:
    233233            # If a model is defined, extract form fields from it.
    234234            fields = fields_for_model(opts.model, opts.fields,
    class BaseModelForm(BaseForm): 
    276276class ModelForm(BaseModelForm):
    277277    __metaclass__ = ModelFormMetaclass
    278278
     279def modelform_for_model(model, form=ModelForm,
     280                        formfield_callback=lambda f: f.formfield(), **options):
     281    opts = model._meta
     282    options.update({"model": model})
     283    return ModelFormMetaclass(opts.object_name + "ModelForm", (form,),
     284                              {}, formfield_callback, **options)
     285
    279286
    280287# Fields #####################################################################
    281288
    class ModelMultipleChoiceField(ModelChoiceField): 
    390397
    391398# Model-FormSet integration ###################################################
    392399
    393 def initial_data(instance, fields=None):
    394     """
    395     Return a dictionary from data in ``instance`` that is suitable for
    396     use as a ``Form`` constructor's ``initial`` argument.
    397 
    398     Provide ``fields`` to specify the names of specific fields to return.
    399     All field values in the instance will be returned if ``fields`` is not
    400     provided.
    401     """
    402     # avoid a circular import
    403     from django.db.models.fields.related import ManyToManyField
    404     opts = instance._meta
    405     initial = {}
    406     for f in opts.fields + opts.many_to_many:
    407         if not f.editable:
    408             continue
    409         if fields and not f.name in fields:
    410             continue
    411         if isinstance(f, ManyToManyField):
    412             # MultipleChoiceWidget needs a list of ints, not object instances.
    413             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
    414         else:
    415             initial[f.name] = f.value_from_object(instance)
    416     return initial
     400class ModelFormSetMetaclass(ModelFormMetaclass):
     401    opts_class = ModelFormSetOptions
    417402
    418403class BaseModelFormSet(BaseFormSet):
    419404    """
    420405    A ``FormSet`` for editing a queryset and/or adding new objects to it.
    421406    """
    422     model = None
    423     queryset = None
    424407
    425     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):
     408    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
     409                 queryset=None):
    426410        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
    427         self.queryset = qs
    428         kwargs['initial'] = [initial_data(obj) for obj in qs]
     411        opts = self._meta
     412        if queryset is None:
     413            self.queryset = self.get_queryset(**kwargs)
     414        else:
     415            self.queryset = queryset
     416        initial_data = []
     417        for obj in self.queryset:
     418            initial_data.append(model_to_dict(obj, opts.fields, opts.exclude))
     419        kwargs['initial'] = initial_data
    429420        super(BaseModelFormSet, self).__init__(**kwargs)
     421   
     422    def get_queryset(self, **kwargs):
     423        """
     424        Hook to returning a queryset for this model.
     425        """
     426        return self._meta.model._default_manager.all()
    430427
    431428    def save_new(self, form, commit=True):
    432429        """Saves and returns a new model instance for the given form."""
    433         return save_instance(form, self.model(), commit=commit)
     430        return save_instance(form, self._meta.model(), commit=commit)
    434431
    435432    def save_instance(self, form, instance, commit=True):
    436433        """Saves and returns an existing model instance for the given form."""
    class BaseModelFormSet(BaseFormSet): 
    445442    def save_existing_objects(self, commit=True):
    446443        if not self.queryset:
    447444            return []
    448         # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
     445        # Put the objects from self.queryset into a dict so they are easy to lookup by pk
    449446        existing_objects = {}
     447        opts = self._meta
    450448        for obj in self.queryset:
    451449            existing_objects[obj.pk] = obj
    452450        saved_instances = []
    453451        for form in self.change_forms:
    454             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
    455             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     452            obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]]
     453            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    456454                obj.delete()
    457455            else:
    458456                saved_instances.append(self.save_instance(form, obj, commit=commit))
    class BaseModelFormSet(BaseFormSet): 
    460458
    461459    def save_new_objects(self, commit=True):
    462460        new_objects = []
     461        opts = self._meta
    463462        for form in self.add_forms:
    464463            if form.is_empty():
    465464                continue
    466465            # If someone has marked an add form for deletion, don't save the
    467466            # object. At some point it would be nice if we didn't display
    468467            # the deletion widget for add forms.
    469             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     468            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    470469                continue
    471470            new_objects.append(self.save_new(form, commit=commit))
    472471        return new_objects
    473472
    474473    def add_fields(self, form, index):
    475474        """Add a hidden field for the object's primary key."""
    476         self._pk_field_name = self.model._meta.pk.attname
     475        self._pk_field_name = self._meta.model._meta.pk.attname
    477476        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    478477        super(BaseModelFormSet, self).add_fields(form, index)
    479478
    480 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
    481                       formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
     479class ModelFormSet(BaseModelFormSet):
     480    __metaclass__ = ModelFormSetMetaclass
     481
     482def formset_for_model(model, formset=BaseModelFormSet,
     483                      formfield_callback=lambda f: f.formfield(), **options):
    482484    """
    483485    Returns a FormSet class for the given Django model class. This FormSet
    484486    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 
    487489    This is essentially the same as ``formset_for_queryset``, but automatically
    488490    uses the model's default manager to determine the queryset.
    489491    """
    490     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
    491     FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    492     FormSet.model = model
    493     return FormSet
     492    opts = model._meta
     493    options.update({"model": model})
     494    return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,),
     495                                 {}, formfield_callback, **options)
     496
     497class InlineFormSetMetaclass(ModelFormSetMetaclass):
     498    opts_class = InlineFormSetOptions
     499
     500    def __new__(cls, name, bases, attrs,
     501                formfield_callback=lambda f: f.formfield(), **options):
     502        formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs,
     503            formfield_callback, **options)
     504        # If this isn't a subclass of InlineFormSet, don't do anything special.
     505        try:
     506            if not filter(lambda b: issubclass(b, InlineFormSet), bases):
     507                return formset
     508        except NameError:
     509            # 'InlineFormSet' isn't defined yet, meaning we're looking at
     510            # Django's own InlineFormSet class, defined below.
     511            return formset
     512        opts = formset._meta
     513        # resolve the foreign key
     514        fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name)
     515        # remove the fk from base_fields to keep it transparent to the form.
     516        try:
     517            del formset.base_fields[fk.name]
     518        except KeyError:
     519            pass
     520        formset.fk = fk
     521        return formset
     522
     523    def _resolve_foreign_key(cls, parent_model, model, fk_name=None):
     524        """
     525        Finds and returns the ForeignKey from model to parent if there is one.
     526        If fk_name is provided, assume it is the name of the ForeignKey field.
     527        """
     528        # avoid a circular import
     529        from django.db.models import ForeignKey
     530        opts = model._meta
     531        if fk_name:
     532            fks_to_parent = [f for f in opts.fields if f.name == fk_name]
     533            if len(fks_to_parent) == 1:
     534                fk = fks_to_parent[0]
     535                if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
     536                    raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
     537            elif len(fks_to_parent) == 0:
     538                raise Exception("%s has no field named '%s'" % (model, fk_name))
     539        else:
     540            # Try to discover what the ForeignKey from model to parent_model is
     541            fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
     542            if len(fks_to_parent) == 1:
     543                fk = fks_to_parent[0]
     544            elif len(fks_to_parent) == 0:
     545                raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
     546            else:
     547                raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
     548        return fk
     549    resolve_foreign_key = classmethod(_resolve_foreign_key)
    494550
    495 class InlineFormset(BaseModelFormSet):
     551class BaseInlineFormSet(BaseModelFormSet):
    496552    """A formset for child objects related to a parent."""
    497     def __init__(self, instance, data=None, files=None):
     553    def __init__(self, *args, **kwargs):
    498554        from django.db.models.fields.related import RelatedObject
    499         self.instance = instance
     555        opts = self._meta
     556        self.instance = kwargs.pop("instance", None)
    500557        # is there a better way to get the object descriptor?
    501         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    502         qs = self.get_queryset()
    503         super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)
     558        rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name()
     559        kwargs["prefix"] = rel_name
     560        super(BaseInlineFormSet, self).__init__(*args, **kwargs)
    504561
    505     def get_queryset(self):
     562    def get_queryset(self, **kwargs):
    506563        """
    507564        Returns this FormSet's queryset, but restricted to children of
    508565        self.instance
    509566        """
    510         kwargs = {self.fk.name: self.instance}
    511         return self.model._default_manager.filter(**kwargs)
     567        opts = self._meta
     568        if self.instance is None:
     569            return opts.model._default_manager.none()
     570        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     571        return queryset.filter(**{self.fk.name: self.instance})
    512572
    513573    def save_new(self, form, commit=True):
    514574        kwargs = {self.fk.get_attname(): self.instance.pk}
    515         new_obj = self.model(**kwargs)
     575        new_obj = self._meta.model(**kwargs)
    516576        return save_instance(form, new_obj, commit=commit)
    517577
    518 def get_foreign_key(parent_model, model, fk_name=None):
    519     """
    520     Finds and returns the ForeignKey from model to parent if there is one.
    521     If fk_name is provided, assume it is the name of the ForeignKey field.
    522     """
    523     # avoid circular import
    524     from django.db.models import ForeignKey
    525     opts = model._meta
    526     if fk_name:
    527         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    528         if len(fks_to_parent) == 1:
    529             fk = fks_to_parent[0]
    530             if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
    531                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    532         elif len(fks_to_parent) == 0:
    533             raise Exception("%s has no field named '%s'" % (model, fk_name))
    534     else:
    535         # Try to discover what the ForeignKey from model to parent_model is
    536         fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
    537         if len(fks_to_parent) == 1:
    538             fk = fks_to_parent[0]
    539         elif len(fks_to_parent) == 0:
    540             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    541         else:
    542             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    543     return fk
     578class InlineFormSet(BaseInlineFormSet):
     579    __metaclass__ = InlineFormSetMetaclass
    544580
    545 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
     581def inline_formset(parent_model, model, formset=InlineFormSet,
     582                   formfield_callback=lambda f: f.formfield(), **options):
    546583    """
    547     Returns an ``InlineFormset`` for the given kwargs.
     584    Returns an ``InlineFormSet`` for the given kwargs.
    548585
    549586    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    550587    to ``parent_model``.
    551588    """
    552     fk = get_foreign_key(parent_model, model, fk_name=fk_name)
    553     # let the formset handle object deletion by default
    554     FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
    555                                 formfield_callback=formfield_callback,
    556                                 extra=extra, orderable=orderable,
    557                                 deletable=deletable)
    558     # HACK: remove the ForeignKey to the parent from every form
    559     # This should be done a line above before we pass 'fields' to formset_for_model
    560     # an 'omit' argument would be very handy here
    561     try:
    562         del FormSet.form_class.base_fields[fk.name]
    563     except KeyError:
    564         pass
    565     FormSet.fk = fk
    566     return FormSet
     589    opts = model._meta
     590    options.update({"parent_model": parent_model, "model": model})
     591    return InlineFormSetMetaclass(opts.object_name + "InlineFormSet", (formset,),
     592                                  {}, formfield_callback, **options)
  • tests/modeltests/model_formsets/models.py

    diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
    index 19bdeed..208509e 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..f7dec6b 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