Ticket #6632: 02-forms-inlines.2.diff

File 02-forms-inlines.2.diff, 35.9 KB (added by Petr Marhoun <petr.marhoun@…>, 16 years ago)
  • 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 django.utils.translation import ugettext as _
    55from fields import IntegerField, BooleanField
    66from widgets import Media, HiddenInput
    7 from util import ErrorList, ValidationError
     7from util import ValidationError
     8import metaclassing
    89
    9 __all__ = ('BaseFormSet', 'all_valid')
     10__all__ = ('BaseFormSet', 'FormSet', 'formset_factory', 'all_valid')
    1011
    1112# special field names
    1213TOTAL_FORM_COUNT = 'TOTAL_FORMS'
     
    2526        self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
    2627        super(ManagementForm, self).__init__(*args, **kwargs)
    2728
     29class FormSetOptions(FormOptions):
     30    def __init__(self, options=None):
     31        super(FormSetOptions, self).__init__(options)
     32        # form
     33        self.form = getattr(options, 'form', None)
     34        self.base_form = getattr(options, 'base_form', Form)
     35        # other options
     36        self.can_delete = getattr(options, 'can_delete', False)
     37        self.can_order = getattr(options, 'can_order', False)
     38        self.extra = getattr(options, 'extra', 1)
     39        self.fieldset_attrs = getattr(options, 'fieldset_attrs', None)
     40        self.fieldset_legend = getattr(options, 'fieldset_legend', None)
     41        self.max_num = getattr(options, 'max_num', 0)
     42        self.output_type = getattr(options, 'output_type', 'tr')
     43        # self.output_type = getattr(options, 'output_type', 'original_table') # backward-compatible
     44
     45class FormSetMetaclass(type):
     46    def __new__(cls, name, bases, attrs):
     47        new_class = type.__new__(cls, name, bases, attrs)
     48        metaclassing.create_meta(new_class, attrs)
     49        metaclassing.create_form_if_not_exists(new_class, attrs)
     50        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
     51        return new_class
     52
    2853class BaseFormSet(StrAndUnicode):
    2954    """
    3055    A collection of instances of the same Form class.
    3156    """
    3257    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    33                  initial=None, error_class=ErrorList):
     58                 initial=None, error_class=None):
    3459        self.is_bound = data is not None or files is not None
    3560        self.prefix = prefix or 'form'
    3661        self.auto_id = auto_id
    3762        self.data = data
    3863        self.files = files
    3964        self.initial = initial
    40         self.error_class = error_class
    41         self._errors = None
    42         self._non_form_errors = None
     65        if error_class is not None:
     66            self.error_class = error_class
     67        self._is_valid = None # Stores validation state after full_clean() has been called.
    4368        # initialization is different depending on whether we recieved data, initial, or nothing
    4469        if data or files:
    4570            self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
     
    6792        self._construct_forms()
    6893
    6994    def __unicode__(self):
    70         return self.as_table()
     95        return getattr(self, 'as_%s' % self.output_type)()
    7196
    7297    def _construct_forms(self):
    7398        # instantiate all the forms and put them in self.forms
     
    182207        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
    183208        are none.
    184209        """
    185         if self._non_form_errors is not None:
    186             return self._non_form_errors
    187         return self.error_class()
     210        if self._is_valid is None:
     211            self.full_clean()
     212        return self._non_form_errors
    188213
    189214    def _get_errors(self):
    190215        """
    191216        Returns a list of form.errors for every form in self.forms.
    192217        """
    193         if self._errors is None:
     218        if self._is_valid is None:
    194219            self.full_clean()
    195220        return self._errors
    196221    errors = property(_get_errors)
     
    199224        """
    200225        Returns True if form.errors is empty for every form in self.forms.
    201226        """
    202         if not self.is_bound:
    203             return False
    204         # We loop over every form.errors here rather than short circuiting on the
    205         # first failure to make sure validation gets triggered for every form.
    206         forms_valid = True
    207         for errors in self.errors:
    208             if bool(errors):
    209                 forms_valid = False
    210         return forms_valid and not bool(self.non_form_errors())
     227        if self._is_valid is None:
     228            self.full_clean()
     229        return self._is_valid
    211230
    212231    def full_clean(self):
    213232        """
    214233        Cleans all of self.data and populates self._errors.
    215234        """
     235        self._is_valid = True # Assume the form is valid until proven otherwise.
    216236        self._errors = []
     237        self._non_form_errors = self.error_class()
    217238        if not self.is_bound: # Stop further processing.
     239            self._is_valid = False
    218240            return
    219241        for i in range(0, self._total_form_count):
    220242            form = self.forms[i]
    221243            self._errors.append(form.errors)
     244            if form.errors:
     245                self._is_valid = False
    222246        # Give self.clean() a chance to do cross-form validation.
    223247        try:
    224248            self.clean()
    225249        except ValidationError, e:
    226             self._non_form_errors = e.messages
     250            self._non_form_errors = self.error_class(e.messages)
     251            self._is_valid = False
    227252
    228253    def clean(self):
    229254        """
     
    265290    media = property(_get_media)
    266291
    267292    def as_table(self):
     293        "Returns this form rendered as HTML <tr>s."
     294        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)
     295
     296    def as_ul(self):
     297        "Returns this form rendered as HTML <li>s."
     298        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)
     299
     300    def as_p(self):
     301        "Returns this form rendered as HTML <p>s."
     302        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)
     303
     304    def as_tr(self):
     305        "Returns this form rendered as HTML <td>s."
     306        output = [self.management_form.as_tr()]
     307        if self.non_form_errors:
     308            output.append(u'<tr><td colspan="%s">%s</td></tr>' % (
     309                len([bf for bf in self.forms[0] if not bf.is_hidden]),
     310                self._html_output_non_form_errors(),
     311            )) 
     312        if self.forms:
     313            output.append(u'<tr>')
     314            output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden)
     315            output.append(u'</tr>')
     316        output.extend(form.as_tr() for form in self.forms)
     317        return '\n'.join(output)
     318   
     319    def _html_output_non_form_errors(self):
     320        if self.non_form_errors:
     321            return u'<div>%s</div>' % unicode(self.non_form_errors())
     322        else:
     323            return u''
     324
     325    def as_original_table(self):
    268326        "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
    269327        # XXX: there is no semantic division between forms here, there
    270328        # probably should be. It might make sense to render each form as a
     
    272330        forms = u' '.join([form.as_table() for form in self.forms])
    273331        return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
    274332
    275 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
    276                     can_delete=False, max_num=0):
     333class FormSet(BaseFormSet):
     334    __metaclass__ = FormSetMetaclass
     335    _options = FormSetOptions
     336
     337def formset_factory(form, formset=FormSet, extra=1, can_order=False,
     338                    can_delete=False, max_num=0, **kwargs):
    277339    """Return a FormSet for the given form class."""
    278     attrs = {'form': form, 'extra': extra,
    279              'can_order': can_order, 'can_delete': can_delete,
    280              'max_num': max_num}
    281     return type(form.__name__ + 'FormSet', (formset,), attrs)
     340    kwargs.update(locals())
     341    meta_class = type('Meta', (), kwargs)
     342    bases = (formset in (FormSet, BaseFormSet) and (FormSet,) or (formset, FormSet))
     343    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
    282344
    283345def all_valid(formsets):
    284346    """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.get_parent_list():
     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'
     
    99
    1010from util import ValidationError
    1111from forms import FormOptions, FormMetaclass, BaseForm
     12from formsets import FormSetOptions, FormSetMetaclass
    1213from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
    1314from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
    14 from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
     15from formsets import BaseFormSet, DELETION_FIELD_NAME
    1516import metaclassing
    1617
    1718__all__ = (
    1819    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    1920    'save_instance', 'form_for_fields', 'ModelChoiceField',
    20     'ModelMultipleChoiceField',
     21    'ModelMultipleChoiceField', 'BaseModelForm', 'ModelForm',
     22    'BaseInlineFormSet', 'InlineFormSet', 'modelform_factory',
     23    'modelformset_factory', 'inlineformset_factory',
    2124)
    2225
    2326def save_instance(form, instance, fields=None, fail_message='saved',
     
    160163        metaclassing.create_declared_fields(new_class, attrs)
    161164        metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
    162165        metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
     166        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
    163167        metaclassing.create_media(new_class, attrs)
    164168        return new_class
    165169
     
    181185        super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
    182186                                            error_class, label_suffix, empty_permitted)
    183187
     188    def _construct_inlines(self):
     189        # this class can create inlines which are subclass of BaseInlineFormSet
     190        self.inlines = []
     191        if self.has_fieldsets():
     192            for fieldset in self._meta.fieldsets:
     193                if not isinstance(fieldset, dict):
     194                    if not issubclass(fieldset, BaseInlineFormSet):
     195                        raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__))
     196                    self.inlines.append(fieldset(self.data, self.files, self.instance))
     197
    184198    def save(self, commit=True):
    185199        """
    186200        Saves this ``form``'s cleaned_data into model instance
     
    193207            fail_message = 'created'
    194208        else:
    195209            fail_message = 'changed'
    196         return save_instance(self, self.instance, self.fields.keys(), fail_message, commit)
     210        self.saved_instance = save_instance(self, self.instance, self.fields.keys(), fail_message, commit)
     211        self.saved_inline_instances = [inline.save(commit) for inline in self.inlines]
     212        return self.saved_instance
    197213
    198214class ModelForm(BaseModelForm):
    199215    __metaclass__ = ModelFormMetaclass
    200216    _options = ModelFormOptions
    201217
    202218def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
    203                        formfield_callback=lambda f: f.formfield()):
    204     # HACK: we should be able to construct a ModelForm without creating
    205     # and passing in a temporary inner class
    206     class Meta:
    207         pass
    208     setattr(Meta, 'model', model)
    209     setattr(Meta, 'fields', fields)
    210     setattr(Meta, 'exclude', exclude)
    211     class_name = model.__name__ + 'Form'
    212     return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
    213                               'formfield_callback': formfield_callback})
     219                       formfield_callback=lambda f: f.formfield(), **kwargs):
     220    kwargs.update(locals())
     221    meta_class = type('Meta', (), kwargs)
     222    bases = (form in (ModelForm, BaseModelForm) and (ModelForm,) or (form, ModelForm))
     223    return ModelFormMetaclass(model.__name__ + 'Form', bases,
     224        {'Meta': meta_class, 'formfield_callback': formfield_callback})
    214225
    215226
    216227# ModelFormSets ##############################################################
    217228
     229class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
     230    def __init__(self, options=None):
     231        super(ModelFormSetOptions, self).__init__(options)
     232        # options changed compared to superclass
     233        self.base_form = getattr(options, 'base_form', ModelForm)
     234
    218235class BaseModelFormSet(BaseFormSet):
    219236    """
    220237    A ``FormSet`` for editing a queryset and/or adding new objects to it.
     
    303320            form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    304321        super(BaseModelFormSet, self).add_fields(form, index)
    305322
     323class ModelFormSet(BaseModelFormSet):
     324    __metaclass__ = FormSetMetaclass # no changes are needed
     325    _options =  ModelFormSetOptions
     326
    306327def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
    307                          formset=BaseModelFormSet,
     328                         formset=ModelFormSet,
    308329                         extra=1, can_delete=False, can_order=False,
    309                          max_num=0, fields=None, exclude=None):
     330                         max_num=0, fields=None, exclude=None, **kwargs):
    310331    """
    311332    Returns a FormSet class for the given Django model class.
    312333    """
    313     form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
    314                              formfield_callback=formfield_callback)
    315     FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
    316                               can_order=can_order, can_delete=can_delete)
    317     FormSet.model = model
    318     return FormSet
     334    kwargs.update(locals())
     335    kwargs['form'] = modelform_factory(**kwargs)
     336    meta_class = type('Meta', (), kwargs)
     337    bases = (formset in (ModelFormSet, BaseModelFormSet) and (ModelFormSet,) or (formset, ModelFormSet))
     338    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
    319339
    320340
    321341# InlineFormSets #############################################################
    322342
     343class InlineFormSetOptions(ModelFormSetOptions):
     344    def __init__(self, options=None):
     345        super(InlineFormSetOptions, self).__init__(options)
     346        self.parent_model = getattr(options, 'parent_model', None)
     347        self.fk_name = getattr(options, 'fk_name', None)
     348        # options changed compared to superclass
     349        self.can_delete = getattr(options, 'can_delete', True)
     350        self.extra = getattr(options, 'extra', 3)
     351
     352class InlineFormSetMetaclass(FormSetMetaclass):
     353    def __new__(cls, name, bases, attrs):
     354        new_class = type.__new__(cls, name, bases, attrs)
     355        metaclassing.create_meta(new_class, attrs)
     356        metaclassing.create_form_if_not_exists(new_class, attrs)
     357        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
     358        metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class, attrs)
     359        return new_class
     360
    323361class BaseInlineFormSet(BaseModelFormSet):
    324362    """A formset for child objects related to a parent."""
    325363    def __init__(self, data=None, files=None, instance=None,
     
    350388        new_obj = self.model(**kwargs)
    351389        return save_instance(form, new_obj, commit=commit)
    352390
    353 def _get_foreign_key(parent_model, model, fk_name=None):
    354     """
    355     Finds and returns the ForeignKey from model to parent if there is one.
    356     If fk_name is provided, assume it is the name of the ForeignKey field.
    357     """
    358     # avoid circular import
    359     from django.db.models import ForeignKey
    360     opts = model._meta
    361     if fk_name:
    362         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    363         if len(fks_to_parent) == 1:
    364             fk = fks_to_parent[0]
    365             if not isinstance(fk, ForeignKey) or \
    366                     (fk.rel.to != parent_model and
    367                      fk.rel.to not in parent_model._meta.get_parent_list()):
    368                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    369         elif len(fks_to_parent) == 0:
    370             raise Exception("%s has no field named '%s'" % (model, fk_name))
    371     else:
    372         # Try to discover what the ForeignKey from model to parent_model is
    373         fks_to_parent = [
    374             f for f in opts.fields
    375             if isinstance(f, ForeignKey)
    376             and (f.rel.to == parent_model
    377                 or f.rel.to in parent_model._meta.get_parent_list())
    378         ]
    379         if len(fks_to_parent) == 1:
    380             fk = fks_to_parent[0]
    381         elif len(fks_to_parent) == 0:
    382             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    383         else:
    384             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    385     return fk
    386 
     391class InlineFormSet(BaseInlineFormSet):
     392    __metaclass__ = InlineFormSetMetaclass
     393    _options = InlineFormSetOptions
    387394
    388395def inlineformset_factory(parent_model, model, form=ModelForm,
    389                           formset=BaseInlineFormSet, fk_name=None,
     396                          formset=InlineFormSet, fk_name=None,
    390397                          fields=None, exclude=None,
    391398                          extra=3, can_order=False, can_delete=True, max_num=0,
    392                           formfield_callback=lambda f: f.formfield()):
     399                          formfield_callback=lambda f: f.formfield(), **kwargs):
    393400    """
    394401    Returns an ``InlineFormSet`` for the given kwargs.
    395402
    396403    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    397404    to ``parent_model``.
    398405    """
    399     fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
    400     # let the formset handle object deletion by default
    401 
    402     if exclude is not None:
    403         exclude.append(fk.name)
    404     else:
    405         exclude = [fk.name]
    406     FormSet = modelformset_factory(model, form=form,
    407                                     formfield_callback=formfield_callback,
    408                                     formset=formset,
    409                                     extra=extra, can_delete=can_delete, can_order=can_order,
    410                                     fields=fields, exclude=exclude, max_num=max_num)
    411     FormSet.fk = fk
    412     return FormSet
     406    kwargs.update(locals())
     407    kwargs['form'] = modelform_factory(**kwargs)
     408    meta_class = type('Meta', (), kwargs)
     409    bases = (formset in (InlineFormSet, BaseInlineFormSet) and (InlineFormSet,) or (form, InlineFormSet))
     410    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class})
    413411
    414412
    415413# Fields #####################################################################
  • tests/modeltests/model_formsets/models.py

    === modified file 'tests/modeltests/model_formsets/models.py'
     
    440440<p><label for="id_form-0-my_pk">My pk:</label> <input id="id_form-0-my_pk" type="text" name="form-0-my_pk" maxlength="10" /></p>
    441441<p><label for="id_form-0-some_field">Some field:</label> <input id="id_form-0-some_field" type="text" name="form-0-some_field" maxlength="100" /></p>
    442442
    443 # Foreign keys in parents ########################################
    444 
    445 >>> from django.forms.models import _get_foreign_key
    446 
    447 >>> type(_get_foreign_key(Restaurant, Owner))
    448 <class 'django.db.models.fields.related.ForeignKey'>
    449 >>> type(_get_foreign_key(MexicanRestaurant, Owner))
    450 <class 'django.db.models.fields.related.ForeignKey'>
    451 
    452443"""}
  • 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