Django

Code

Ticket #6241: formset_refactor_7125.diff

File formset_refactor_7125.diff, 49.5 kB (added by jkocherhans, 10 months 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.

  • a/django/contrib/admin/options.py

    old new  
    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 
     
    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        """ 
     
    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        """ 
     
    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) 
     
    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. 
     
    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.""" 
     
    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' 
     
    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()): 
     
    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    """ 
  • a/django/contrib/admin/templates/admin/edit_inline/tabular.html

    old new  
    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 
  • a/django/newforms/formsets.py

    old new  
    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' 
     
    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 
     
    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): 
     
    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 
     
    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): 
     
    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 
     
    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) 
     
    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: 
     
    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): 
     
    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 
  • a/django/newforms/models.py

    old new  
    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 
     
    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 
     
    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, 
     
    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 
     
    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.""" 
     
    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)) 
     
    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 
     
    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) 
  • a/tests/modeltests/model_formsets/models.py

    old new  
    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> 
     
    3559...     'form-2-name': '', 
    3660... } 
    3761 
    38 >>> formset = AuthorFormSet(qs, data=data) 
     62>>> formset = AuthorFormSet(data) 
    3963>>> formset.is_valid() 
    4064True 
    4165 
     
    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> 
     
    73104...     'form-2-name': 'Paul Verlaine', 
    74105... } 
    75106 
    76 >>> formset = AuthorFormSet(qs, data=data) 
     107>>> formset = AuthorFormSet(data) 
    77108>>> formset.is_valid() 
    78109True 
    79110 
     
    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> 
     
    117155...     'form-3-DELETE': 'on', 
    118156... } 
    119157 
    120 >>> formset = AuthorFormSet(qs, data=data) 
     158>>> formset = AuthorFormSet(data) 
    121159>>> formset.is_valid() 
    122160True 
    123161 
     
    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> 
     
    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 
     
    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> 
     
    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 
  • a/tests/regressiontests/forms/formsets.py

    old new  
    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, 
     
    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" /&