Ticket #5372: formset_refactor_3.diff

File formset_refactor_3.diff, 47.2 KB (added by brosner, 8 years ago)

fixes the real problem. formsets are declarative like forms. 85% done, but attaching to get feedback.

  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index c3289ea..19f599b 100644
    a b class ModelAdmin(BaseModelAdmin): 
    331331        "Hook for specifying fieldsets for the change form."
    332332        if self.declared_fieldsets:
    333333            return self.declared_fieldsets
    334         form = self.form_change(request, obj)
     334        form = self.form_change(request)
    335335        return [(None, {'fields': form.base_fields.keys()})]
    336336
    337337    def form_add(self, request):
    class ModelAdmin(BaseModelAdmin): 
    342342            fields = flatten_fieldsets(self.declared_fieldsets)
    343343        else:
    344344            fields = None
    345         return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
     345        return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    346346
    347     def form_change(self, request, obj):
     347    def form_change(self, request):
    348348        """
    349349        Returns a Form class for use in the admin change view.
    350350        """
    class ModelAdmin(BaseModelAdmin): 
    352352            fields = flatten_fieldsets(self.declared_fieldsets)
    353353        else:
    354354            fields = None
    355         return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)
     355        return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    356356
    357357    def save_add(self, request, model, form, formsets, post_url_continue):
    358358        """
    class ModelAdmin(BaseModelAdmin): 
    492492            # Object list will give 'Permission Denied', so go back to admin home
    493493            post_url = '../../../'
    494494
    495         ModelForm = self.form_add(request)
     495        Form = self.form_add(request)
    496496        inline_formsets = []
    497497        obj = self.model()
    498498        if request.method == 'POST':
    499             form = ModelForm(request.POST, request.FILES)
     499            form = Form(request.POST, request.FILES)
    500500            for FormSet in self.formsets_add(request):
    501                 inline_formset = FormSet(obj, data=request.POST, files=request.FILES)
     501                inline_formset = FormSet(request.POST, request.FILES)
    502502                inline_formsets.append(inline_formset)
    503503            if all_valid(inline_formsets) and form.is_valid():
    504504                return self.save_add(request, model, form, inline_formsets, '../%s/')
    505505        else:
    506             form = ModelForm(initial=request.GET)
     506            form = Form(initial=request.GET)
    507507            for FormSet in self.formsets_add(request):
    508                 inline_formset = FormSet(obj)
     508                inline_formset = FormSet()
    509509                inline_formsets.append(inline_formset)
    510510
    511511        adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
    class ModelAdmin(BaseModelAdmin): 
    552552        if request.POST and request.POST.has_key("_saveasnew"):
    553553            return self.add_view(request, form_url='../../add/')
    554554
    555         ModelForm = self.form_change(request, obj)
     555        Form = self.form_change(request)
    556556        inline_formsets = []
    557557        if request.method == 'POST':
    558             form = ModelForm(request.POST, request.FILES)
     558            form = Form(request.POST, request.FILES)
    559559            for FormSet in self.formsets_change(request, obj):
    560                 inline_formset = FormSet(obj, request.POST, request.FILES)
     560                inline_formset = FormSet(request.POST, request.FILES, instance=obj)
    561561                inline_formsets.append(inline_formset)
    562562
    563563            if all_valid(inline_formsets) and form.is_valid():
    564564                return self.save_change(request, model, form, inline_formsets)
    565565        else:
    566             form = ModelForm()
     566            form = Form(instance=obj, initial=request.GET)
    567567            for FormSet in self.formsets_change(request, obj):
    568                 inline_formset = FormSet(obj)
     568                inline_formset = FormSet(instance=obj)
    569569                inline_formsets.append(inline_formset)
    570570
    571571        ## Populate the FormWrapper.
  • django/newforms/formsets.py

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

    diff --git a/django/newforms/models.py b/django/newforms/models.py
    index 3c9b43d..419389a 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    'modelform_for_model', 'formset_for_model', 'inline_formset',
    2324    'ModelChoiceField', 'ModelMultipleChoiceField',
    2425)
    2526
    def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda 
    209210            field_list.append((f.name, formfield))
    210211    return SortedDict(field_list)
    211212
    212 class ModelFormOptions(object):
    213     def __init__(self, options=None):
    214         self.model = getattr(options, 'model', None)
    215         self.fields = getattr(options, 'fields', None)
    216         self.exclude = getattr(options, 'exclude', None)
    217 
    218213class ModelFormMetaclass(type):
    219     def __new__(cls, name, bases, attrs):
    220         # TODO: no way to specify formfield_callback yet, do we need one, or
    221         # should it be a special case for the admin?
     214    def __new__(cls, name, bases, attrs,
     215                formfield_callback=lambda f: f.formfield(), **options):
    222216        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
    223217        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
    224218
    class ModelFormMetaclass(type): 
    230224                fields = base.base_fields.items() + fields
    231225        declared_fields = SortedDict(fields)
    232226
    233         opts = ModelFormOptions(attrs.get('Meta', None))
     227        opts = cls.get_options(options and options or attrs.get('Meta', None))
    234228        attrs['_meta'] = opts
    235229
    236230        # Don't allow more than one Meta model defenition in bases. The fields
    class ModelFormMetaclass(type): 
    262256        else:
    263257            attrs['base_fields'] = declared_fields
    264258        return type.__new__(cls, name, bases, attrs)
     259   
     260    def _get_options(cls, options=None):
     261        """
     262        Returns the options instance for this class.
     263        """
     264        return ModelFormOptions(options)
     265    get_options = classmethod(_get_options)
    265266
    266267class BaseModelForm(BaseForm):
    267268    def __init__(self, instance, data=None, files=None, auto_id='id_%s', prefix=None,
    class BaseModelForm(BaseForm): 
    290291class ModelForm(BaseModelForm):
    291292    __metaclass__ = ModelFormMetaclass
    292293
     294# this should really be named form_for_model.
     295def modelform_for_model(model, form=BaseModelForm,
     296                        formfield_callback=lambda f: f.formfield(), **options):
     297    opts = model._meta
     298    options.update({"model": model})
     299    return ModelFormMetaclass(opts.object_name + "ModelForm", (form,),
     300                              {}, **options)
     301
    293302
    294303# Fields #####################################################################
    295304
    class ModelMultipleChoiceField(ModelChoiceField): 
    404413
    405414# Model-FormSet integration ###################################################
    406415
    407 def initial_data(instance, fields=None):
    408     """
    409     Return a dictionary from data in ``instance`` that is suitable for
    410     use as a ``Form`` constructor's ``initial`` argument.
    411 
    412     Provide ``fields`` to specify the names of specific fields to return.
    413     All field values in the instance will be returned if ``fields`` is not
    414     provided.
    415     """
    416     # avoid a circular import
    417     from django.db.models.fields.related import ManyToManyField
    418     opts = instance._meta
    419     initial = {}
    420     for f in opts.fields + opts.many_to_many:
    421         if not f.editable:
    422             continue
    423         if fields and not f.name in fields:
    424             continue
    425         if isinstance(f, ManyToManyField):
    426             # MultipleChoiceWidget needs a list of ints, not object instances.
    427             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
    428         else:
    429             initial[f.name] = f.value_from_object(instance)
    430     return initial
     416class ModelFormSetMetaclass(ModelFormMetaclass):
     417    def _get_options(cls, options=None):
     418        return ModelFormSetOptions(options)
     419    get_options = classmethod(_get_options)
    431420
    432421class BaseModelFormSet(BaseFormSet):
    433422    """
    434423    A ``FormSet`` for editing a queryset and/or adding new objects to it.
    435424    """
    436     model = None
    437     queryset = None
    438425
    439     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):
     426    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None):
    440427        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
    441         self.queryset = qs
    442         kwargs['initial'] = [initial_data(obj) for obj in qs]
     428        opts = self._meta
     429        self.queryset = self.get_queryset(**kwargs)
     430        initial_data = []
     431        for obj in self.queryset:
     432            initial_data.append(model_to_dict(obj, opts.fields, opts.exclude))
     433        kwargs['initial'] = initial_data
    443434        super(BaseModelFormSet, self).__init__(**kwargs)
     435   
     436    def get_queryset(self, **kwargs):
     437        """
     438        Hook to returning a queryset for this model.
     439        """
     440        return self._meta.model._default_manager.all()
    444441
    445442    def save_new(self, form, commit=True):
    446443        """Saves and returns a new model instance for the given form."""
    447         return save_instance(form, self.model(), commit=commit)
     444        return save_instance(form, self._meta.model(), commit=commit)
    448445
    449446    def save_instance(self, form, instance, commit=True):
    450447        """Saves and returns an existing model instance for the given form."""
    class BaseModelFormSet(BaseFormSet): 
    461458            return []
    462459        # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
    463460        existing_objects = {}
     461        opts = self._meta
    464462        for obj in self.queryset:
    465463            existing_objects[obj.pk] = obj
    466464        saved_instances = []
    467465        for form in self.change_forms:
    468             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
    469             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     466            obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]]
     467            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    470468                obj.delete()
    471469            else:
    472470                saved_instances.append(self.save_instance(form, obj, commit=commit))
    class BaseModelFormSet(BaseFormSet): 
    474472
    475473    def save_new_objects(self, commit=True):
    476474        new_objects = []
     475        opts = self._meta
    477476        for form in self.add_forms:
    478477            if form.is_empty():
    479478                continue
    480479            # If someone has marked an add form for deletion, don't save the
    481480            # object. At some point it would be nice if we didn't display
    482481            # the deletion widget for add forms.
    483             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     482            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    484483                continue
    485484            new_objects.append(self.save_new(form, commit=commit))
    486485        return new_objects
    487486
    488487    def add_fields(self, form, index):
    489488        """Add a hidden field for the object's primary key."""
    490         self._pk_field_name = self.model._meta.pk.attname
     489        self._pk_field_name = self._meta.model._meta.pk.attname
    491490        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    492491        super(BaseModelFormSet, self).add_fields(form, index)
    493492
    494 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
    495                       formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
     493class ModelFormSet(BaseModelFormSet):
     494    __metaclass__ = ModelFormSetMetaclass
     495
     496def formset_for_model(model, formset=BaseModelFormSet,
     497                      formfield_callback=lambda f: f.formfield(), **options):
    496498    """
    497499    Returns a FormSet class for the given Django model class. This FormSet
    498500    will contain change forms for every instance of the given model as well
    def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formf 
    501503    This is essentially the same as ``formset_for_queryset``, but automatically
    502504    uses the model's default manager to determine the queryset.
    503505    """
    504     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
    505     FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    506     FormSet.model = model
    507     return FormSet
     506    opts = model._meta
     507    options.update({"model": model})
     508    return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,),
     509                                 {}, **options)
    508510
    509 class InlineFormset(BaseModelFormSet):
     511class InlineFormSetMetaclass(ModelFormMetaclass):
     512    def _get_options(cls, options=None):
     513        return InlineFormSetOptions(options)
     514    get_options = classmethod(_get_options)
     515
     516class BaseInlineFormSet(BaseModelFormSet):
    510517    """A formset for child objects related to a parent."""
    511     def __init__(self, instance, data=None, files=None):
     518    def __init__(self, *args, **kwargs):
    512519        from django.db.models.fields.related import RelatedObject
    513         self.instance = instance
     520        opts = self._meta
     521        self.instance = kwargs.pop("instance", None)
     522        self.fk = self.get_foreign_key(opts.parent_model, opts.model, opts.fk_name)
    514523        # is there a better way to get the object descriptor?
    515         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    516         qs = self.get_queryset()
    517         super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)
     524        rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name()
     525        kwargs["prefix"] = rel_name
     526        # remove the foreign key from base_fields
     527        try:
     528            del self.base_fields[self.fk.name]
     529        except KeyError:
     530            pass
     531        super(BaseInlineFormSet, self).__init__(*args, **kwargs)
    518532
    519     def get_queryset(self):
     533    def get_queryset(self, **kwargs):
    520534        """
    521535        Returns this FormSet's queryset, but restricted to children of
    522536        self.instance
    523537        """
    524         kwargs = {self.fk.name: self.instance}
    525         return self.model._default_manager.filter(**kwargs)
     538        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     539        return queryset.filter(**{self.fk.name: self.instance})
     540   
     541    def get_foreign_key(self, parent_model, model, fk_name=None):
     542        """
     543        Finds and returns the ForeignKey from model to parent if there is one.
     544        If fk_name is provided, assume it is the name of the ForeignKey field.
     545        """
     546        from django.db.models import ForeignKey
     547        opts = model._meta
     548        if fk_name:
     549            fks_to_parent = [f for f in opts.fields if f.name == fk_name]
     550            if len(fks_to_parent) == 1:
     551                fk = fks_to_parent[0]
     552                if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
     553                    raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
     554            elif len(fks_to_parent) == 0:
     555                raise Exception("%s has no field named '%s'" % (model, fk_name))
     556        else:
     557            # Try to discover what the ForeignKey from model to parent_model is
     558            fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
     559            if len(fks_to_parent) == 1:
     560                fk = fks_to_parent[0]
     561            elif len(fks_to_parent) == 0:
     562                raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
     563            else:
     564                raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
     565        return fk
    526566
    527567    def save_new(self, form, commit=True):
    528568        kwargs = {self.fk.get_attname(): self.instance.pk}
    529         new_obj = self.model(**kwargs)
     569        new_obj = self._meta.model(**kwargs)
    530570        return save_instance(form, new_obj, commit=commit)
    531571
    532 def get_foreign_key(parent_model, model, fk_name=None):
    533     """
    534     Finds and returns the ForeignKey from model to parent if there is one.
    535     If fk_name is provided, assume it is the name of the ForeignKey field.
    536     """
    537     # avoid circular import
    538     from django.db.models import ForeignKey
    539     opts = model._meta
    540     if fk_name:
    541         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    542         if len(fks_to_parent) == 1:
    543             fk = fks_to_parent[0]
    544             if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
    545                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    546         elif len(fks_to_parent) == 0:
    547             raise Exception("%s has no field named '%s'" % (model, fk_name))
    548     else:
    549         # Try to discover what the ForeignKey from model to parent_model is
    550         fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
    551         if len(fks_to_parent) == 1:
    552             fk = fks_to_parent[0]
    553         elif len(fks_to_parent) == 0:
    554             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    555         else:
    556             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    557     return fk
     572class InlineFormset(BaseInlineFormSet):
     573    __metaclass__ = InlineFormSetMetaclass
    558574
    559 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
     575def inline_formset(parent_model, model, formset=BaseInlineFormSet,
     576                   formfield_callback=lambda f: f.formfield(), **options):
    560577    """
    561578    Returns an ``InlineFormset`` for the given kwargs.
    562579
    563580    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    564581    to ``parent_model``.
    565582    """
    566     fk = get_foreign_key(parent_model, model, fk_name=fk_name)
    567     # let the formset handle object deletion by default
    568     FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
    569                                 formfield_callback=formfield_callback,
    570                                 extra=extra, orderable=orderable,
    571                                 deletable=deletable)
    572     # HACK: remove the ForeignKey to the parent from every form
    573     # This should be done a line above before we pass 'fields' to formset_for_model
    574     # an 'omit' argument would be very handy here
    575     try:
    576         del FormSet.form_class.base_fields[fk.name]
    577     except KeyError:
    578         pass
    579     FormSet.fk = fk
    580     return FormSet
     583    opts = model._meta
     584    options.update({"parent_model": parent_model, "model": model})
     585    return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,),
     586                                  {}, **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..4370aa7
    - +  
     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.fk_name = self._dynamic_attribute(options, "fk_name")
     49   
     50 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..baaa38f 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.newforms.models import formset_for_model, ModelFormSet
    2020
    21 >>> qs = Author.objects.all()
    22 >>> AuthorFormSet = formset_for_model(Author, extra=3)
     21Lets create a formset that is bound to a model.
    2322
    24 >>> formset = AuthorFormSet(qs)
     23>>> class AuthorFormSet(ModelFormSet):
     24...     class Meta:
     25...         model = Author
     26...         num_extra = 3
     27
     28>>> formset = AuthorFormSet()
    2529>>> for form in formset.forms:
    2630...     print form.as_p()
    2731<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': """ 
    3539...     'form-2-name': '',
    3640... }
    3741
    38 >>> formset = AuthorFormSet(qs, data=data)
     42>>> formset = AuthorFormSet(data)
    3943>>> formset.is_valid()
    4044True
    4145
    Charles Baudelaire 
    4953
    5054
    5155Gah! 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)
     56authors with an extra form to add him. When subclassing ModelFormSet you can
     57override the get_queryset method to return any queryset we like, but in this
     58case we'll use it to display it in alphabetical order by name.
     59
     60>>> class AuthorFormSet(ModelFormSet):
     61...     class Meta:
     62...         model = Author
     63...         num_extra = 1
     64...         deletable = False
     65...
     66...     def get_queryset(self, **kwargs):
     67...         qs = super(AuthorFormSet, self).get_queryset(**kwargs)
     68...         return qs.order_by('name')
     69
     70>>> formset = AuthorFormSet()
    6071>>> for form in formset.forms:
    6172...     print form.as_p()
    6273<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. 
    7384...     'form-2-name': 'Paul Verlaine',
    7485... }
    7586
    76 >>> formset = AuthorFormSet(qs, data=data)
     87>>> formset = AuthorFormSet(data)
    7788>>> formset.is_valid()
    7889True
    7990
    Paul Verlaine 
    90101This probably shouldn't happen, but it will. If an add form was marked for
    91102deltetion, make sure we don't save that form.
    92103
    93 >>> qs = Author.objects.order_by('name')
    94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
    95 
    96 >>> formset = AuthorFormSet(qs)
     104>>> class AuthorFormSet(ModelFormSet):
     105...     class Meta:
     106...         model = Author
     107...         num_extra = 1
     108...         deletable = True
     109...
     110...     def get_queryset(self, **kwargs):
     111...         qs = super(AuthorFormSet, self).get_queryset(**kwargs)
     112...         return qs.order_by('name')
     113
     114>>> formset = AuthorFormSet()
    97115>>> for form in formset.forms:
    98116...     print form.as_p()
    99117<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. 
    117135...     'form-3-DELETE': 'on',
    118136... }
    119137
    120 >>> formset = AuthorFormSet(qs, data=data)
     138>>> formset = AuthorFormSet(data)
    121139>>> formset.is_valid()
    122140True
    123141
    Paul Verlaine 
    134152We can also create a formset that is tied to a parent model. This is how the
    135153admin system's edit inline functionality works.
    136154
    137 >>> from django.newforms.models import inline_formset
     155>>> from django.newforms.models import inline_formset, InlineFormset
     156
     157>>> class AuthorBooksFormSet(InlineFormset):
     158...     class Meta:
     159...         parent_model = Author
     160...         model = Book
     161...         num_extra = 3
     162...         deletable = False
    138163
    139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
    140164>>> author = Author.objects.get(name='Charles Baudelaire')
    141165
    142 >>> formset = AuthorBooksFormSet(author)
     166>>> formset = AuthorBooksFormSet(instance=author)
    143167>>> for form in formset.forms:
    144168...     print form.as_p()
    145169<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. 
    153177...     'book_set-2-title': '',
    154178... }
    155179
    156 >>> formset = AuthorBooksFormSet(author, data=data)
     180>>> formset = AuthorBooksFormSet(data, instance=author)
    157181>>> formset.is_valid()
    158182True
    159183
    Now that we've added a book to Charles Baudelaire, let's try adding another 
    169193one. This time though, an edit form will be available for every existing
    170194book.
    171195
    172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
     196>>> class AuthorBooksFormSet(InlineFormset):
     197...     class Meta:
     198...         parent_model = Author
     199...         model = Book
     200...         num_extra = 2
     201...         deletable = False
     202
    173203>>> author = Author.objects.get(name='Charles Baudelaire')
    174204
    175 >>> formset = AuthorBooksFormSet(author)
     205>>> formset = AuthorBooksFormSet(instance=author)
    176206>>> for form in formset.forms:
    177207...     print form.as_p()
    178208<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. 
    187217...     'book_set-2-title': '',
    188218... }
    189219
    190 >>> formset = AuthorBooksFormSet(author, data=data)
     220>>> formset = AuthorBooksFormSet(data, instance=author)
    191221>>> formset.is_valid()
    192222True
    193223
  • tests/regressiontests/forms/formsets.py

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

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

    diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
    index 6275135..cdc6c8b 100644
    a b displayed because you forgot to add it to fields/fielsets 
    7272>>> ma = BandAdmin(Band, site)
    7373>>> ma.form_add(request).base_fields.keys()
    7474['name']
    75 >>> ma.form_change(request, band).base_fields.keys()
     75>>> ma.form_change(request).base_fields.keys()
    7676['name']
    7777
    7878>>> class BandAdmin(ModelAdmin):
    displayed because you forgot to add it to fields/fielsets 
    8181>>> ma = BandAdmin(Band, site)
    8282>>> ma.form_add(request).base_fields.keys()
    8383['name']
    84 >>> ma.form_change(request, band).base_fields.keys()
     84>>> ma.form_change(request).base_fields.keys()
    8585['name']
    8686
    8787
Back to Top