Ticket #6632: 02-forms-inlines.diff

File 02-forms-inlines.diff, 36.6 KB (added by Petr Marhoun <petr.marhoun@…>, 7 years ago)
  • django/contrib/admin/options.py

    === modified file 'django/contrib/admin/options.py'
     
    11from django import forms, template
    22from django.forms.formsets import all_valid
    33from django.forms.models import modelform_factory, inlineformset_factory
    4 from django.forms.models import BaseInlineFormset
     4from django.forms.models import BaseInlineFormSet
    55from django.contrib.contenttypes.models import ContentType
    66from django.contrib.admin import widgets
    77from django.contrib.admin.util import quote, unquote, get_deleted_objects
     
    714714    """
    715715    model = None
    716716    fk_name = None
    717     formset = BaseInlineFormset
     717    formset = BaseInlineFormSet
    718718    extra = 3
    719719    max_num = 0
    720720    template = None
  • django/forms/__init__.py

    === modified file 'django/forms/__init__.py'
     
    1414from widgets import *
    1515from fields import *
    1616from forms import *
     17from formsets import *
    1718from models import *
  • django/forms/forms.py

    === modified file 'django/forms/forms.py'
     
    3030        self.fieldsets = getattr(options, 'fieldsets', None)
    3131        self.fields = getattr(options, 'fields', None)
    3232        self.exclude = getattr(options, 'exclude', None)
     33        self.inlines = getattr(options, 'inlines', None)
    3334        # other options
    3435        self.error_class = getattr(options, 'error_class', ErrorList)
    3536        self.error_row_class = getattr(options, 'error_row_class', 'error')
     
    3839        self.label_capfirst = getattr(options, 'label_capfirst', True)
    3940        # self.label_capfirst = getattr(options, 'label_capfirst', False) # backward-compatible
    4041        self.label_suffix = getattr(options, 'label_suffix', ':')
     42        self.output_type = getattr(options, 'output_type', 'table')
    4143        self.required_row_class = getattr(options, 'required_row_class', 'required')
    4244        # self.required_row_class = getattr(options, 'required_row_class', None) # backward-compatible
    4345        self.use_field_row_class = getattr(options, 'use_field_row_class', True)
     
    5052        metaclassing.create_declared_fields(new_class, attrs)
    5153        metaclassing.create_base_fields_pool_from_declared_fields(new_class, attrs)
    5254        metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
     55        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
    5356        metaclassing.create_media(new_class, attrs)
    5457        return new_class
    5558
     
    7578        if label_suffix is not None:
    7679            self.label_suffix = label_suffix
    7780        self.empty_permitted = empty_permitted
    78         self._errors = None # Stores the errors after clean() has been called.
     81        self._is_valid = None # Stores validation state after full_clean() has been called.
    7982        self._changed_data = None
    80 
    8183        # The base_fields class attribute is the *class-wide* definition of
    8284        # fields. Because a particular *instance* of the class might want to
    8385        # alter self.fields, we create self.fields here by copying base_fields.
    8486        # Instances should always modify self.fields; they should not modify
    8587        # self.base_fields.
    8688        self.fields = deepcopy(self.base_fields)
     89        self._construct_inlines()
     90
     91    def _construct_inlines(self):
     92        # this class cannot create any inlines
     93        self.inlines = []
     94        if self.has_fieldsets():
     95            for fieldset in self._meta.fieldsets:
     96                if not isinstance(fieldset, dict):
     97                    raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__))
    8798
    8899    def __unicode__(self):
    89         return self.as_table()
     100        return getattr(self, 'as_%s' % self.output_type)()
    90101
    91102    def __iter__(self):
    92103        for name, field in self.fields.items():
     
    102113
    103114    def _get_errors(self):
    104115        "Returns an ErrorDict for the data provided for the form."
    105         if self._errors is None:
     116        if self._is_valid is None:
    106117            self.full_clean()
    107118        return self._errors
    108119    errors = property(_get_errors)
     
    112123        Returns True if the form has no errors. Otherwise, False. If errors are
    113124        being ignored, returns False.
    114125        """
    115         return self.is_bound and not bool(self.errors)
     126        if self._is_valid is None:
     127            self.full_clean()
     128        return self._is_valid
    116129
    117130    def add_prefix(self, name):
    118131        """
     
    129142
    130143    def first_fieldset_attrs(self):
    131144        "Returns attributes for first fieldset as HTML code."
    132         if self.has_fieldsets() and 'attrs' in self._meta.fieldsets[0]:
    133             return flatatt(self._meta.fieldsets[0]['attrs'])
     145        if self.has_fieldsets():
     146            if isinstance(self._meta.fieldsets[0], dict):
     147                attrs = self._meta.fieldsets[0].get('attrs')
     148            else:
     149                attrs = self.inlines[0].fieldset_attrs
     150        else:
     151            attrs = None
     152        if attrs:
     153            return flatatt(attrs)
    134154        else:
    135155            return u''
    136156
    137157    def first_fieldset_legend_tag(self):
    138158        "Returns legend tag for first fieldset as HTML code."
    139         if self.has_fieldsets() and 'legend' in self._meta.fieldsets[0]:
    140             return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(self._meta.fieldsets[0]['legend'])))
     159        if self.has_fieldsets():
     160            if isinstance(self._meta.fieldsets[0], dict):
     161                legend = self._meta.fieldsets[0].get('legend')
     162            else:
     163                legend = self.inlines[0].fieldset_legend
     164        else:
     165            legend = None
     166        if legend:
     167            return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(legend)))
    141168        else:
    142169            return u''
    143170
     
    205232            output.append(fieldset_end_html)
    206233        return u'\n'.join(output)
    207234
     235    def _inline_html_output(self, inline, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html):
     236        "Helper function for outputting HTML from a inline. Used by _html_output."
     237        output = []
     238        if not is_first:
     239            legend_tag = attrs = u''
     240            if inline.fieldset_legend:
     241                legend_tag = legend_tag_html % {
     242                    'legend': conditional_escape(force_unicode(inline.fieldset_legend)),
     243                }
     244            if inline.fieldset_attrs:
     245                attrs = flatatt(inline.fieldset_attrs)
     246            output.append(fieldset_start_html % {
     247                'legend_tag': legend_tag,
     248                'attrs': attrs,
     249            })
     250        output.append(unicode(inline))
     251        if not is_last:
     252            output.append(fieldset_end_html)
     253        return u'\n'.join(output)
     254
    208255    def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html):
    209256        "Helper function for outputting HTML from a hidden fields. Used by _html_output."
    210257        if self.hidden_row_class:
     
    232279        if top_errors:
    233280            output.append(self._top_errors_html_output(top_errors, top_errors_html))
    234281        if self.has_fieldsets():
     282            inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines.
    235283            for i, fieldset in enumerate(self._meta.fieldsets):
    236                 fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
    237284                is_first = (i == 0)
    238285                is_last = (i + 1 == len(self._meta.fieldsets))
    239                 output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
    240                     fieldset_start_html, fieldset_end_html, legend_tag_html))
     286                if isinstance(fieldset, dict):
     287                    fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
     288                    output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
     289                        fieldset_start_html, fieldset_end_html, legend_tag_html))
     290                else:
     291                    output.append(self._inline_html_output(inlines.pop(0), is_first, is_last,
     292                        fieldset_start_html, fieldset_end_html, legend_tag_html))
    241293        else:
    242294            for name in self.fields:
    243295                if name in visible_fields:
     
    288340        }
    289341        return self._html_output(**kwargs)
    290342
     343    def as_tr(self):
     344        "Returns this form rendered as HTML <td>s."
     345        if self.has_fieldsets():
     346            raise ValueError("%s has fieldsets or inlines so its method as_tr cannot be used." % self.__class__.__name__)
     347        colspan = len([bf for bf in self if not bf.is_hidden])
     348        kwargs = {
     349            'row_html': u'<td%(attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>',
     350            'label_tag_html': u'',
     351            'help_text_html': u' %(help_text)s',
     352            'top_errors_html': u'<tr><td colspan="%s">%%(top_errors)s</td></tr>\n<tr>' % colspan,
     353            'fieldset_start_html': u'',
     354            'fieldset_end_html': u'',
     355            'legend_tag_html': u'',
     356            'hidden_fields_html': u'</tr>\n<tr%%(attrs)s><td colspan="%s">%%(hidden_fields)s</td></tr>' % colspan,
     357        }
     358        html_output = self._html_output(**kwargs)
     359        if not html_output.startswith('<tr>'):
     360            html_output = u'<tr>\n%s' % html_output
     361        if not html_output.endswith('</tr>'):
     362            html_output = u'%s\n</tr>' % html_output
     363        return html_output
     364
    291365    def non_field_errors(self):
    292366        """
    293367        Returns an ErrorList of errors that aren't associated with a particular
    294368        field -- i.e., from Form.clean(). Returns an empty ErrorList if there
    295369        are none.
    296370        """
     371        if self._is_valid is None:
     372            self.full_clean()
    297373        return self.errors.get(NON_FIELD_ERRORS, self.error_class())
    298374
    299375    def full_clean(self):
    300376        """
    301         Cleans all of self.data and populates self._errors and
     377        Cleans all of self.data and populates self._is_valid, self._errors and
    302378        self.cleaned_data.
    303379        """
     380        self._is_valid = True # Assume the form is valid until proven otherwise.
    304381        self._errors = ErrorDict()
    305382        if not self.is_bound: # Stop further processing.
     383            self._is_valid = False
    306384            return
    307385        self.cleaned_data = {}
    308386        # If the form is permitted to be empty, and none of the form data has
     
    328406                self._errors[name] = self.error_class(e.messages)
    329407                if name in self.cleaned_data:
    330408                    del self.cleaned_data[name]
     409                self._is_valid = False
    331410        try:
    332411            self.cleaned_data = self.clean()
    333412        except ValidationError, e:
    334413            self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
    335         if self._errors:
     414            self._is_valid = False
     415        for inline in self.inlines:
     416            inline.full_clean()
     417            if not inline.is_valid():
     418                self._is_valid = False
     419        if not self._is_valid:
    336420            delattr(self, 'cleaned_data')
     421            for inline in self.inlines:
     422                inline._is_valid = False
    337423
    338424    def clean(self):
    339425        """
     
    375461        media = Media()
    376462        for field in self.fields.values():
    377463            media = media + field.widget.media
     464        for inline in self.inlines:
     465            media = media + inline.media
    378466        return media
    379467    media = property(_get_media)
    380468
    381469    def is_multipart(self):
    382         """
    383         Returns True if the form needs to be multipart-encrypted, i.e. it has
    384         FileInput. Otherwise, False.
    385         """
     470       
     471        """
     472        Returns True if the form (including inlines) needs to be
     473        multipart-encrypted, i.e. it has FileInput. Otherwise, False.
     474         """
    386475        for field in self.fields.values():
    387476            if field.widget.needs_multipart_form:
    388477                return True
     478        for inline in self.inlines:
     479            if inline.is_multipart():
     480                return True
    389481        return False
    390482
    391483class Form(BaseForm):
  • django/forms/formsets.py

    === modified file 'django/forms/formsets.py'
     
    1 from forms import Form
     1from forms import Form, FormOptions
    22from django.utils.encoding import StrAndUnicode
    33from django.utils.safestring import mark_safe
    44from fields import IntegerField, BooleanField
    55from widgets import Media, HiddenInput
    6 from util import ErrorList, ValidationError
     6from util import ValidationError
     7import metaclassing
    78
    8 __all__ = ('BaseFormSet', 'all_valid')
     9__all__ = ('BaseFormSet', 'FormSet', 'formset_factory', 'all_valid')
    910
    1011# special field names
    1112TOTAL_FORM_COUNT = 'TOTAL_FORMS'
     
    2425        self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
    2526        super(ManagementForm, self).__init__(*args, **kwargs)
    2627
     28class FormSetOptions(FormOptions):
     29    def __init__(self, options=None):
     30        super(FormSetOptions, self).__init__(options)
     31        # form
     32        self.form = getattr(options, 'form', None)
     33        self.base_form = getattr(options, 'base_form', Form)
     34        # other options
     35        self.can_delete = getattr(options, 'can_delete', False)
     36        self.can_order = getattr(options, 'can_order', False)
     37        self.extra = getattr(options, 'extra', 1)
     38        self.fieldset_attrs = getattr(options, 'fieldset_attrs', None)
     39        self.fieldset_legend = getattr(options, 'fieldset_legend', None)
     40        self.max_num = getattr(options, 'max_num', 0)
     41        self.output_type = getattr(options, 'output_type', 'tr')
     42        # self.output_type = getattr(options, 'output_type', 'original_table') # backward-compatible
     43
     44class FormSetMetaclass(type):
     45    def __new__(cls, name, bases, attrs):
     46        new_class = type.__new__(cls, name, bases, attrs)
     47        metaclassing.create_meta(new_class, attrs)
     48        metaclassing.create_form_if_not_exists(new_class, attrs)
     49        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
     50        return new_class
     51
    2752class BaseFormSet(StrAndUnicode):
    2853    """
    2954    A collection of instances of the same Form class.
    3055    """
    3156    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    32                  initial=None, error_class=ErrorList):
     57                 initial=None, error_class=None):
    3358        self.is_bound = data is not None or files is not None
    3459        self.prefix = prefix or 'form'
    3560        self.auto_id = auto_id
    3661        self.data = data
    3762        self.files = files
    3863        self.initial = initial
    39         self.error_class = error_class
    40         self._errors = None
    41         self._non_form_errors = None
     64        if error_class is not None:
     65            self.error_class = error_class
     66        self._is_valid = None # Stores validation state after full_clean() has been called.
    4267        # initialization is different depending on whether we recieved data, initial, or nothing
    4368        if data or files:
    4469            self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
     
    6691        self._construct_forms()
    6792
    6893    def __unicode__(self):
    69         return self.as_table()
     94        return getattr(self, 'as_%s' % self.output_type)()
    7095
    7196    def _construct_forms(self):
    7297        # instantiate all the forms and put them in self.forms
     
    181206        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
    182207        are none.
    183208        """
    184         if self._non_form_errors is not None:
    185             return self._non_form_errors
    186         return self.error_class()
     209        if self._is_valid is None:
     210            self.full_clean()
     211        return self._non_form_errors
    187212
    188213    def _get_errors(self):
    189214        """
    190215        Returns a list of form.errors for every form in self.forms.
    191216        """
    192         if self._errors is None:
     217        if self._is_valid is None:
    193218            self.full_clean()
    194219        return self._errors
    195220    errors = property(_get_errors)
     
    198223        """
    199224        Returns True if form.errors is empty for every form in self.forms.
    200225        """
    201         if not self.is_bound:
    202             return False
    203         # We loop over every form.errors here rather than short circuiting on the
    204         # first failure to make sure validation gets triggered for every form.
    205         forms_valid = True
    206         for errors in self.errors:
    207             if bool(errors):
    208                 forms_valid = False
    209         return forms_valid and not bool(self.non_form_errors())
     226        if self._is_valid is None:
     227            self.full_clean()
     228        return self._is_valid
    210229
    211230    def full_clean(self):
    212231        """
    213232        Cleans all of self.data and populates self._errors.
    214233        """
     234        self._is_valid = True # Assume the form is valid until proven otherwise.
    215235        self._errors = []
     236        self._non_form_errors = self.error_class()
    216237        if not self.is_bound: # Stop further processing.
     238            self._is_valid = False
    217239            return
    218240        for i in range(0, self._total_form_count):
    219241            form = self.forms[i]
    220242            self._errors.append(form.errors)
     243            if form.errors:
     244                self._is_valid = False
    221245        # Give self.clean() a chance to do cross-form validation.
    222246        try:
    223247            self.clean()
    224248        except ValidationError, e:
    225             self._non_form_errors = e.messages
     249            self._non_form_errors = self.error_class(e.messages)
     250            self._is_valid = False
    226251
    227252    def clean(self):
    228253        """
     
    264289    media = property(_get_media)
    265290
    266291    def as_table(self):
     292        "Returns this form rendered as HTML <tr>s."
     293        return self._html_output_non_form_errors() + u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms)
     294
     295    def as_ul(self):
     296        "Returns this form rendered as HTML <li>s."
     297        return self._html_output_non_form_errors() + u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms)
     298
     299    def as_p(self):
     300        "Returns this form rendered as HTML <p>s."
     301        return self._html_output_non_form_errors() + u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms)
     302
     303    def as_tr(self):
     304        "Returns this form rendered as HTML <td>s."
     305        output = [self.management_form.as_tr()]
     306        if self.non_form_errors:
     307            output.append(u'<tr><td colspan="%s">%s</td></tr>' % (
     308                len([bf for bf in self.forms[0] if not bf.is_hidden]),
     309                self._html_output_non_form_errors(),
     310            )) 
     311        if self.forms:
     312            output.append(u'<tr>')
     313            output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden)
     314            output.append(u'</tr>')
     315        output.extend(form.as_tr() for form in self.forms)
     316        return '\n'.join(output)
     317   
     318    def _html_output_non_form_errors(self):
     319        if self.non_form_errors:
     320            return u'<div>%s</div>' % unicode(self.non_form_errors())
     321        else:
     322            return u''
     323
     324    def as_original_table(self):
    267325        "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
    268326        # XXX: there is no semantic division between forms here, there
    269327        # probably should be. It might make sense to render each form as a
     
    271329        forms = u' '.join([form.as_table() for form in self.forms])
    272330        return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
    273331
    274 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
    275                     can_delete=False, max_num=0):
     332class FormSet(BaseFormSet):
     333    __metaclass__ = FormSetMetaclass
     334    _options = FormSetOptions
     335
     336def formset_factory(form, formset=FormSet, extra=1, can_order=False,
     337                    can_delete=False, max_num=0, **kwargs):
    276338    """Return a FormSet for the given form class."""
    277     attrs = {'form': form, 'extra': extra,
    278              'can_order': can_order, 'can_delete': can_delete,
    279              'max_num': max_num}
    280     return type(form.__name__ + 'FormSet', (formset,), attrs)
     339    kwargs.update(locals())
     340    meta_class = type('Meta', (), kwargs)
     341    bases = (formset == FormSet and (FormSet,) or (formset, FormSet))
     342    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
    281343
    282344def all_valid(formsets):
    283345    """Returns true if every formset in formsets is valid."""
  • django/forms/metaclassing.py

    === modified file 'django/forms/metaclassing.py'
     
    1111def create_meta(cls, attrs):
    1212    cls._meta = cls._options(getattr(cls, 'Meta', None))
    1313    for name, attr in cls._meta.__dict__.items():
    14         if name not in ('fieldsets', 'fields', 'exclude'):
     14        if name not in ('fieldsets', 'fields', 'exclude', 'inlines', 'base_form'):
    1515            setattr(cls, name, attr)
    1616
    1717def create_declared_fields(cls, attrs):
     
    6060    if cls._meta.fieldsets:
    6161        names = []
    6262        for fieldset in cls._meta.fieldsets:
    63             names.extend(fieldset['fields'])
     63            if isinstance(fieldset, dict):
     64                names.extend(fieldset['fields'])
    6465    elif cls._meta.fields:
    6566        names = cls._meta.fields
    6667    elif cls._meta.exclude:
     
    7273def create_media(cls, attrs):
    7374    if not 'media' in attrs:
    7475        cls.media = media_property(cls)
     76
     77def create_fieldsets_if_inlines_exist(cls, attrs):
     78    if cls._meta.inlines is not None:
     79        if cls._meta.fieldsets is not None:
     80            raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__)
     81        cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines)
     82
     83def create_form_if_not_exists(cls, attrs):
     84    if not cls.form:
     85        form_attrs = {
     86            'Meta': type('Meta', (), cls._meta.__dict__),
     87        }
     88        for name, possible_field in attrs.items():
     89            if isinstance(possible_field, Field):
     90                form_attrs[name] = possible_field
     91                delattr(cls, name)
     92        cls.form = type(cls.__name__ + 'Form', (cls._meta.base_form,), form_attrs)
     93
     94def check_no_fieldsets_in_inner_form(cls, attrs):
     95    if cls.form._meta.fieldsets:
     96        raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__)
     97
     98def add_fk_attribute_and_remove_fk_from_base_fields(cls, attrs):
     99    # If models are not set, this class would not be used directly.
     100    if not (cls.parent_model and cls.model):
     101        return
     102    # Try to discover what the foreign key from model to parent_model is.
     103    fks_to_parent = []
     104    for field in cls.model._meta.fields:
     105        # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy.
     106        try:
     107            if field.rel.to == cls.parent_model or field.rel.to in cls.parent_model._meta.parents.keys():
     108                fks_to_parent.append(field)
     109        except AttributeError:
     110            pass
     111    if cls.fk_name:
     112        fks_to_parent = [fk for fk in fks_to_parent if fk.name == cls.fk_name]
     113        if len(fks_to_parent) == 0:
     114            raise ImproperlyConfigured("%s has no ForeignKey with name %s to %s." %
     115                (cls.model, cls.fk_name, cls.parent_model))
     116        elif len(fks_to_parent) > 1:
     117            raise ImproperlyConfigured("%s has more than one ForeignKey with name %s to %s." %
     118                (cls.model, cls.fk_name, cls.parent_model))
     119    else:
     120        if len(fks_to_parent) == 0:
     121            raise ImproperlyConfigured("%s has no ForeignKey to %s." %
     122                (cls.model, cls.parent_model))
     123        if len(fks_to_parent) > 1:
     124            raise ImproperlyConfigured("%s has more than one ForeignKey to %s." %
     125                (cls.model, cls.parent_model))
     126    cls.fk = fks_to_parent[0]
     127    # Try to remove the foreign key from base_fields to keep it transparent to the form.
     128    try:
     129        del cls.form.base_fields[cls.fk.name]
     130    except KeyError:
     131        pass
  • django/forms/models.py

    === modified file 'django/forms/models.py'
     
    1111
    1212from util import ValidationError
    1313from forms import FormOptions, FormMetaclass, BaseForm
     14from formsets import FormSetOptions, FormSetMetaclass
    1415from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
    1516from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
    16 from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
     17from formsets import BaseFormSet, DELETION_FIELD_NAME
    1718import metaclassing
    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     'ModelChoiceField', 'ModelMultipleChoiceField',
     23    'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelForm',
     24    'ModelForm', 'BaseInlineFormSet', 'InlineFormSet', 'modelform_factory',
     25    'modelformset_factory', 'inlineformset_factory',
    2326)
    2427
    2528def save_instance(form, instance, fields=None, fail_message='saved',
     
    219222        metaclassing.create_declared_fields(new_class, attrs)
    220223        metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
    221224        metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
     225        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
    222226        metaclassing.create_media(new_class, attrs)
    223227        return new_class
    224228
     
    240244        super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
    241245                                            error_class, label_suffix, empty_permitted)
    242246
     247    def _construct_inlines(self):
     248        # this class can create inlines which are subclass of BaseInlineFormSet
     249        self.inlines = []
     250        if self.has_fieldsets():
     251            for fieldset in self._meta.fieldsets:
     252                if not isinstance(fieldset, dict):
     253                    if not issubclass(fieldset, BaseInlineFormSet):
     254                        raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__))
     255                    self.inlines.append(fieldset(self.data, self.files, self.instance))
     256
    243257    def save(self, commit=True):
    244258        """
    245259        Saves this ``form``'s cleaned_data into model instance
     
    252266            fail_message = 'created'
    253267        else:
    254268            fail_message = 'changed'
    255         return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
     269        self.saved_instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
     270        self.saved_inline_instances = [inline.save(commit) for inline in self.inlines]
     271        return self.saved_instance
    256272
    257273class ModelForm(BaseModelForm):
    258274    __metaclass__ = ModelFormMetaclass
    259275    _options = ModelFormOptions
    260276
    261277def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
    262                        formfield_callback=lambda f: f.formfield()):
    263     # HACK: we should be able to construct a ModelForm without creating
    264     # and passing in a temporary inner class
    265     class Meta:
    266         pass
    267     setattr(Meta, 'model', model)
    268     setattr(Meta, 'fields', fields)
    269     setattr(Meta, 'exclude', exclude)
    270     class_name = model.__name__ + 'Form'
    271     return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
    272                               'formfield_callback': formfield_callback})
     278                       formfield_callback=lambda f: f.formfield(), **kwargs):
     279    kwargs.update(locals())
     280    meta_class = type('Meta', (), kwargs)
     281    bases = (form == ModelForm and (ModelForm,) or (form, ModelForm))
     282    return ModelFormMetaclass(model.__name__ + 'Form', bases,
     283        {'Meta': meta_class, 'formfield_callback': formfield_callback})
    273284
    274285
    275286# ModelFormSets ##############################################################
    276287
     288class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
     289    def __init__(self, options=None):
     290        super(ModelFormSetOptions, self).__init__(options)
     291        # options changed compared to superclass
     292        self.base_form = getattr(options, 'base_form', ModelForm)
     293
    277294class BaseModelFormSet(BaseFormSet):
    278295    """
    279296    A ``FormSet`` for editing a queryset and/or adding new objects to it.
     
    362379            form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    363380        super(BaseModelFormSet, self).add_fields(form, index)
    364381
     382class ModelFormSet(BaseModelFormSet):
     383    __metaclass__ = FormSetMetaclass # no changes are needed
     384    _options =  ModelFormSetOptions
     385
    365386def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
    366                          formset=BaseModelFormSet,
     387                         formset=ModelFormSet,
    367388                         extra=1, can_delete=False, can_order=False,
    368                          max_num=0, fields=None, exclude=None):
     389                         max_num=0, fields=None, exclude=None, **kwargs):
    369390    """
    370391    Returns a FormSet class for the given Django model class.
    371392    """
    372     form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
    373                              formfield_callback=formfield_callback)
    374     FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
    375                               can_order=can_order, can_delete=can_delete)
    376     FormSet.model = model
    377     return FormSet
     393    kwargs.update(locals())
     394    kwargs['form'] = modelform_factory(**kwargs)
     395    meta_class = type('Meta', (), kwargs)
     396    bases = (formset == ModelFormSet and (ModelFormSet,) or (formset, ModelFormSet))
     397    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
    378398
    379399
    380400# InlineFormSets #############################################################
    381401
    382 class BaseInlineFormset(BaseModelFormSet):
     402class InlineFormSetOptions(ModelFormSetOptions):
     403    def __init__(self, options=None):
     404        super(InlineFormSetOptions, self).__init__(options)
     405        self.parent_model = getattr(options, 'parent_model', None)
     406        self.fk_name = getattr(options, 'fk_name', None)
     407        # options changed compared to superclass
     408        self.can_delete = getattr(options, 'can_delete', True)
     409        self.extra = getattr(options, 'extra', 3)
     410
     411class InlineFormSetMetaclass(FormSetMetaclass):
     412    def __new__(cls, name, bases, attrs):
     413        new_class = type.__new__(cls, name, bases, attrs)
     414        metaclassing.create_meta(new_class, attrs)
     415        metaclassing.create_form_if_not_exists(new_class, attrs)
     416        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
     417        metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class, attrs)
     418        return new_class
     419
     420class BaseInlineFormSet(BaseModelFormSet):
    383421    """A formset for child objects related to a parent."""
    384422    def __init__(self, data=None, files=None, instance=None,
    385423                 save_as_new=False, prefix=None):
     
    388426        self.save_as_new = save_as_new
    389427        # is there a better way to get the object descriptor?
    390428        self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    391         super(BaseInlineFormset, self).__init__(data, files, prefix=prefix or self.rel_name)
     429        super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name)
    392430   
    393431    def _construct_forms(self):
    394432        if self.save_as_new:
    395433            self._total_form_count = self._initial_form_count
    396434            self._initial_form_count = 0
    397         super(BaseInlineFormset, self)._construct_forms()
     435        super(BaseInlineFormSet, self)._construct_forms()
    398436
    399437    def get_queryset(self):
    400438        """
     
    409447        new_obj = self.model(**kwargs)
    410448        return save_instance(form, new_obj, commit=commit)
    411449
    412 def _get_foreign_key(parent_model, model, fk_name=None):
    413     """
    414     Finds and returns the ForeignKey from model to parent if there is one.
    415     If fk_name is provided, assume it is the name of the ForeignKey field.
    416     """
    417     # avoid circular import
    418     from django.db.models import ForeignKey
    419     opts = model._meta
    420     if fk_name:
    421         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    422         if len(fks_to_parent) == 1:
    423             fk = fks_to_parent[0]
    424             if not isinstance(fk, ForeignKey) or \
    425                     (fk.rel.to != parent_model and
    426                      fk.rel.to not in parent_model._meta.parents.keys()):
    427                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    428         elif len(fks_to_parent) == 0:
    429             raise Exception("%s has no field named '%s'" % (model, fk_name))
    430     else:
    431         # Try to discover what the ForeignKey from model to parent_model is
    432         fks_to_parent = [
    433             f for f in opts.fields
    434             if isinstance(f, ForeignKey)
    435             and (f.rel.to == parent_model
    436                 or f.rel.to in parent_model._meta.parents.keys())
    437         ]
    438         if len(fks_to_parent) == 1:
    439             fk = fks_to_parent[0]
    440         elif len(fks_to_parent) == 0:
    441             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    442         else:
    443             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    444     return fk
    445 
     450class InlineFormSet(BaseInlineFormSet):
     451    __metaclass__ = InlineFormSetMetaclass
     452    _options = InlineFormSetOptions
    446453
    447454def inlineformset_factory(parent_model, model, form=ModelForm,
    448                           formset=BaseInlineFormset, fk_name=None,
     455                          formset=InlineFormSet, fk_name=None,
    449456                          fields=None, exclude=None,
    450457                          extra=3, can_order=False, can_delete=True, max_num=0,
    451                           formfield_callback=lambda f: f.formfield()):
     458                          formfield_callback=lambda f: f.formfield(), **kwargs):
    452459    """
    453460    Returns an ``InlineFormset`` for the given kwargs.
    454461
    455462    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    456463    to ``parent_model``.
    457464    """
    458     fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
    459     # let the formset handle object deletion by default
    460    
    461     if exclude is not None:
    462         exclude.append(fk.name)
    463     else:
    464         exclude = [fk.name]
    465     FormSet = modelformset_factory(model, form=form,
    466                                     formfield_callback=formfield_callback,
    467                                     formset=formset,
    468                                     extra=extra, can_delete=can_delete, can_order=can_order,
    469                                     fields=fields, exclude=exclude, max_num=max_num)
    470     FormSet.fk = fk
    471     return FormSet
     465    kwargs.update(locals())
     466    kwargs['form'] = modelform_factory(**kwargs)
     467    meta_class = type('Meta', (), kwargs)
     468    bases = (formset == InlineFormSet and (InlineFormSet,) or (form, InlineFormSet))
     469    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
    472470
    473471
    474472# Fields #####################################################################
  • tests/regressiontests/inline_formsets/models.py

    === modified file 'tests/regressiontests/inline_formsets/models.py'
     
    2424>>> ifs = inlineformset_factory(Parent, Child)
    2525Traceback (most recent call last):
    2626    ...
    27 Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
     27ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has more than one ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>.
    2828
    2929
    3030These two should both work without a problem.
     
    3939>>> ifs = inlineformset_factory(Parent, Child, fk_name='school')
    4040Traceback (most recent call last):
    4141    ...
    42 Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
     42ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name school to <class 'regressiontests.inline_formsets.models.Parent'>.
    4343
    4444
    4545If the field specified in fk_name is not a ForeignKey, we should get an
     
    4848>>> ifs = inlineformset_factory(Parent, Child, fk_name='test')
    4949Traceback (most recent call last):
    5050    ...
    51 Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
     51ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name test to <class 'regressiontests.inline_formsets.models.Parent'>.
    5252
    5353
    5454"""
Back to Top