Django

Code

Ticket #5372: formset_refactor_3.diff

File formset_refactor_3.diff, 47.2 kB (added by brosner, 1 year ago)

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

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

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

    old new  
    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 
     
    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 
     
    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 
     
    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, 
     
    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 
     
    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.""" 
     
    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)) 
     
    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 
     
    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) 
  • /dev/null

    old new  
     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     
  • 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.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> 
     
    3539...     'form-2-name': '', 
    3640... } 
    3741 
    38 >>> formset = AuthorFormSet(qs, data=data) 
     42>>> formset = AuthorFormSet(data) 
    3943>>> formset.is_valid() 
    4044True 
    4145 
     
    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> 
     
    7384...     'form-2-name': 'Paul Verlaine', 
    7485... } 
    7586 
    76 >>> formset = AuthorFormSet(qs, data=data) 
     87>>> formset = AuthorFormSet(data) 
    7788>>> formset.is_valid() 
    7889True 
    7990 
     
    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> 
     
    117135...     'form-3-DELETE': 'on', 
    118136... } 
    119137 
    120 >>> formset = AuthorFormSet(qs, data=data) 
     138>>> formset = AuthorFormSet(data) 
    121139>>> formset.is_valid() 
    122140True 
    123141 
     
    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> 
     
    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 
     
    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> 
     
    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 
  • 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" /></li> 
     
    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 
     
    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 
     
    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 
     
    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> 
     
    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 
     
    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):