Django

Code

Ticket #6241: formset_refactor_9.diff

File formset_refactor_9.diff, 52.0 kB (added by brosner, 1 year ago)

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

  • 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 
     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    '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  
    216211class ModelFormMetaclass(type): 
     212    opts_class = ModelFormOptions 
     213     
    217214    def __new__(cls, name, bases, attrs, 
    218                 formfield_callback=lambda f: f.formfield()): 
     215                formfield_callback=lambda f: f.formfield(), **options): 
    219216        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 
    220217        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 
    221218 
     
    227224                fields = base.base_fields.items() + fields 
    228225        declared_fields = SortedDict(fields) 
    229226 
    230         opts = ModelFormOptions(attrs.get('Meta', None)) 
     227        opts = cls.opts_class(options and options or attrs.get('Meta', None)) 
    231228        attrs['_meta'] = opts 
    232229 
    233230        # Don't allow more than one Meta model defenition in bases. The fields 
     
    260257        else: 
    261258            attrs['base_fields'] = declared_fields 
    262259        return type.__new__(cls, name, bases, attrs) 
    263  
     260         
    264261class BaseModelForm(BaseForm): 
    265262    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
    266263                 initial=None, error_class=ErrorList, label_suffix=':', instance=None): 
     
    293290class ModelForm(BaseModelForm): 
    294291    __metaclass__ = ModelFormMetaclass 
    295292 
     293def modelform_for_model(model, form=ModelForm, 
     294                        formfield_callback=lambda f: f.formfield(), **options): 
     295    opts = model._meta 
     296    options.update({"model": model}) 
     297    return ModelFormMetaclass(opts.object_name + "ModelForm", (form,), 
     298                              {}, formfield_callback, **options) 
     299 
    296300 
    297301# Fields ##################################################################### 
    298302 
     
    407411 
    408412# Model-FormSet integration ################################################### 
    409413 
    410 def initial_data(instance, fields=None): 
    411     """ 
    412     Return a dictionary from data in ``instance`` that is suitable for 
    413     use as a ``Form`` constructor's ``initial`` argument. 
    414  
    415     Provide ``fields`` to specify the names of specific fields to return. 
    416     All field values in the instance will be returned if ``fields`` is not 
    417     provided. 
    418     """ 
    419     # avoid a circular import 
    420     from django.db.models.fields.related import ManyToManyField 
    421     opts = instance._meta 
    422     initial = {} 
    423     for f in opts.fields + opts.many_to_many: 
    424         if not f.editable: 
    425             continue 
    426         if fields and not f.name in fields: 
    427             continue 
    428         if isinstance(f, ManyToManyField): 
    429             # MultipleChoiceWidget needs a list of ints, not object instances. 
    430             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 
    431         else: 
    432             initial[f.name] = f.value_from_object(instance) 
    433     return initial 
     414class ModelFormSetMetaclass(ModelFormMetaclass): 
     415    opts_class = ModelFormSetOptions 
    434416 
    435417class BaseModelFormSet(BaseFormSet): 
    436418    """ 
    437419    A ``FormSet`` for editing a queryset and/or adding new objects to it. 
    438420    """ 
    439     model = None 
    440     queryset = None 
    441421 
    442     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None): 
     422    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
     423                 queryset=None): 
    443424        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 
    444         self.queryset = qs 
    445         kwargs['initial'] = [initial_data(obj) for obj in qs] 
     425        opts = self._meta 
     426        if queryset is None: 
     427            self.queryset = self.get_queryset(**kwargs) 
     428        else: 
     429            self.queryset = queryset 
     430        initial_data = [] 
     431        for obj in self.queryset: 
     432            initial_data.append(model_to_dict(obj, opts.fields, opts.exclude)) 
     433        kwargs['initial'] = initial_data 
    446434        super(BaseModelFormSet, self).__init__(**kwargs) 
     435     
     436    def get_queryset(self, **kwargs): 
     437        """ 
     438        Hook to returning a queryset for this model. 
     439        """ 
     440        return self._meta.model._default_manager.all() 
    447441 
    448442    def save_new(self, form, commit=True): 
    449443        """Saves and returns a new model instance for the given form.""" 
    450         return save_instance(form, self.model(), commit=commit) 
     444        return save_instance(form, self._meta.model(), commit=commit) 
    451445 
    452446    def save_instance(self, form, instance, commit=True): 
    453447        """Saves and returns an existing model instance for the given form.""" 
     
    462456    def save_existing_objects(self, commit=True): 
    463457        if not self.queryset: 
    464458            return [] 
    465         # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 
     459        # Put the objects from self.queryset into a dict so they are easy to lookup by pk 
    466460        existing_objects = {} 
     461        opts = self._meta 
    467462        for obj in self.queryset: 
    468463            existing_objects[obj.pk] = obj 
    469464        saved_instances = [] 
    470465        for form in self.change_forms: 
    471             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]] 
    472             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
     466            obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]] 
     467            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
    473468                obj.delete() 
    474469            else: 
    475470                saved_instances.append(self.save_instance(form, obj, commit=commit)) 
     
    477472 
    478473    def save_new_objects(self, commit=True): 
    479474        new_objects = [] 
     475        opts = self._meta 
    480476        for form in self.add_forms: 
    481477            if form.is_empty(): 
    482478                continue 
    483479            # If someone has marked an add form for deletion, don't save the 
    484480            # object. At some point it would be nice if we didn't display 
    485481            # the deletion widget for add forms. 
    486             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
     482            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
    487483                continue 
    488484            new_objects.append(self.save_new(form, commit=commit)) 
    489485        return new_objects 
    490486 
    491487    def add_fields(self, form, index): 
    492488        """Add a hidden field for the object's primary key.""" 
    493         self._pk_field_name = self.model._meta.pk.attname 
     489        self._pk_field_name = self._meta.model._meta.pk.attname 
    494490        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput) 
    495491        super(BaseModelFormSet, self).add_fields(form, index) 
    496492 
    497 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 
    498                       formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 
     493class ModelFormSet(BaseModelFormSet): 
     494    __metaclass__ = ModelFormSetMetaclass 
     495 
     496def formset_for_model(model, formset=BaseModelFormSet, 
     497                      formfield_callback=lambda f: f.formfield(), **options): 
    499498    """ 
    500499    Returns a FormSet class for the given Django model class. This FormSet 
    501500    will contain change forms for every instance of the given model as well 
     
    504503    This is essentially the same as ``formset_for_queryset``, but automatically 
    505504    uses the model's default manager to determine the queryset. 
    506505    """ 
    507     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 
    508     FormSet = formset_for_form(form, formset, extra, orderable, deletable
    509     FormSet.model = model 
    510     return FormSet 
     506    opts = model._meta 
     507    options.update({"model": model}
     508    return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,), 
     509                                 {}, formfield_callback, **options) 
    511510 
    512 class InlineFormset(BaseModelFormSet): 
     511class InlineFormSetMetaclass(ModelFormSetMetaclass): 
     512    opts_class = InlineFormSetOptions 
     513     
     514    def __new__(cls, name, bases, attrs, 
     515                formfield_callback=lambda f: f.formfield(), **options): 
     516        formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs, 
     517            formfield_callback, **options) 
     518        # If this isn't a subclass of InlineFormset, don't do anything special. 
     519        try: 
     520            if not filter(lambda b: issubclass(b, InlineFormset), bases): 
     521                return formset 
     522        except NameError: 
     523            # 'InlineFormset' isn't defined yet, meaning we're looking at 
     524            # Django's own InlineFormset class, defined below. 
     525            return formset 
     526        opts = formset._meta 
     527        # resolve the foreign key 
     528        fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name) 
     529        # remove the fk from base_fields to keep it transparent to the form. 
     530        try: 
     531            del formset.base_fields[fk.name] 
     532        except KeyError: 
     533            pass 
     534        formset.fk = fk 
     535        return formset 
     536     
     537    def _resolve_foreign_key(cls, parent_model, model, fk_name=None): 
     538        """ 
     539        Finds and returns the ForeignKey from model to parent if there is one. 
     540        If fk_name is provided, assume it is the name of the ForeignKey field. 
     541        """ 
     542        # avoid a circular import 
     543        from django.db.models import ForeignKey 
     544        opts = model._meta 
     545        if fk_name: 
     546            fks_to_parent = [f for f in opts.fields if f.name == fk_name] 
     547            if len(fks_to_parent) == 1: 
     548                fk = fks_to_parent[0] 
     549                if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 
     550                    raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 
     551            elif len(fks_to_parent) == 0: 
     552                raise Exception("%s has no field named '%s'" % (model, fk_name)) 
     553        else: 
     554            # Try to discover what the ForeignKey from model to parent_model is 
     555            fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 
     556            if len(fks_to_parent) == 1: 
     557                fk = fks_to_parent[0] 
     558            elif len(fks_to_parent) == 0: 
     559                raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 
     560            else: 
     561                raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 
     562        return fk 
     563    resolve_foreign_key = classmethod(_resolve_foreign_key) 
     564 
     565class BaseInlineFormSet(BaseModelFormSet): 
    513566    """A formset for child objects related to a parent.""" 
    514     def __init__(self, instance, data=None, files=None): 
     567    def __init__(self, *args, **kwargs): 
    515568        from django.db.models.fields.related import RelatedObject 
    516         self.instance = instance 
     569        opts = self._meta 
     570        self.instance = kwargs.pop("instance", None) 
    517571        # is there a better way to get the object descriptor? 
    518         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() 
    519         qs = self.get_queryset() 
    520         super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name
     572        rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name() 
     573        kwargs["prefix"] = rel_name 
     574        super(BaseInlineFormSet, self).__init__(*args, **kwargs
    521575 
    522     def get_queryset(self): 
     576    def get_queryset(self, **kwargs): 
    523577        """ 
    524578        Returns this FormSet's queryset, but restricted to children of  
    525579        self.instance 
    526580        """ 
    527         kwargs = {self.fk.name: self.instance} 
    528         return self.model._default_manager.filter(**kwargs) 
     581        opts = self._meta 
     582        if self.instance is None: 
     583            return opts.model._default_manager.none() 
     584        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs) 
     585        return queryset.filter(**{self.fk.name: self.instance}) 
    529586 
    530587    def save_new(self, form, commit=True): 
    531588        kwargs = {self.fk.get_attname(): self.instance.pk} 
    532         new_obj = self.model(**kwargs) 
     589        new_obj = self._meta.model(**kwargs) 
    533590        return save_instance(form, new_obj, commit=commit) 
    534591 
    535 def get_foreign_key(parent_model, model, fk_name=None): 
    536     """ 
    537     Finds and returns the ForeignKey from model to parent if there is one. 
    538     If fk_name is provided, assume it is the name of the ForeignKey field. 
    539     """ 
    540     # avoid circular import 
    541     from django.db.models import ForeignKey 
    542     opts = model._meta 
    543     if fk_name: 
    544         fks_to_parent = [f for f in opts.fields if f.name == fk_name] 
    545         if len(fks_to_parent) == 1: 
    546             fk = fks_to_parent[0] 
    547             if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model: 
    548                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model)) 
    549         elif len(fks_to_parent) == 0: 
    550             raise Exception("%s has no field named '%s'" % (model, fk_name)) 
    551     else: 
    552         # Try to discover what the ForeignKey from model to parent_model is 
    553         fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 
    554         if len(fks_to_parent) == 1: 
    555             fk = fks_to_parent[0] 
    556         elif len(fks_to_parent) == 0: 
    557             raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 
    558         else: 
    559             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 
    560     return fk 
     592class InlineFormset(BaseInlineFormSet): 
     593    __metaclass__ = InlineFormSetMetaclass 
    561594 
    562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()): 
     595def inline_formset(parent_model, model, formset=InlineFormset, 
     596                   formfield_callback=lambda f: f.formfield(), **options): 
    563597    """ 
    564598    Returns an ``InlineFormset`` for the given kwargs. 
    565599 
    566600    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 
    567601    to ``parent_model``. 
    568602    """ 
    569     fk = get_foreign_key(parent_model, model, fk_name=fk_name) 
    570     # let the formset handle object deletion by default 
    571     FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, 
    572                                 formfield_callback=formfield_callback, 
    573                                 extra=extra, orderable=orderable, 
    574                                 deletable=deletable) 
    575     # HACK: remove the ForeignKey to the parent from every form 
    576     # This should be done a line above before we pass 'fields' to formset_for_model 
    577     # an 'omit' argument would be very handy here 
    578     try: 
    579         del FormSet.form_class.base_fields[fk.name] 
    580     except KeyError: 
    581         pass 
    582     FormSet.fk = fk 
    583     return FormSet 
     603    opts = model._meta 
     604    options.update({"parent_model": parent_model, "model": model}) 
     605    return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,), 
     606                                  {}, formfield_callback, **options) 
  • /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.num_extra = self._dynamic_attribute(options, "num_extra", 3) 
     49        self.fk_name = self._dynamic_attribute(options, "fk_name") 
     50        self.deletable = self._dynamic_attribute(options, "deletable", True) 
     51     
  • 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