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

File 02-newforms-inlines.2.diff, 38.7 KB (added by Petr Marhoun <petr.marhoun@…>, 17 years ago)
  • django/newforms/formsets.py

    === added file 'django/newforms/formsets.py'
     
     1from django.utils.encoding import StrAndUnicode
     2from django.utils.translation import ugettext as _
     3
     4from fields import BooleanField, IntegerField
     5from forms import Form, FormOptions
     6from widgets import HiddenInput
     7from util import ValidationError
     8import metaclassing
     9
     10__all__ = ('BaseFormSet', 'FormSet')
     11
     12# Special field names.
     13CHANGE_FORMS_COUNT_FIELD_NAME = 'CHANGE-FORMS-COUNT'
     14ALL_FORMS_COUNT_FIELD_NAME = 'ALL-FORMS-COUNT'
     15ORDERING_FIELD_NAME = 'ORDER'
     16DELETION_FIELD_NAME = 'DELETE'
     17
     18class ManagementForm(Form):
     19    """
     20    ``ManagementForm`` is used to keep track of how many form instances
     21    are displayed on the page. If adding new forms via javascript, you should
     22    increment the count field of this form as well.
     23    """
     24    def __init__(self, *args, **kwargs):
     25        self.base_fields[CHANGE_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
     26        self.base_fields[ALL_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
     27        super(ManagementForm, self).__init__(*args, **kwargs)
     28
     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.deletable = getattr(options, 'deletable', False)
     37        self.is_empty_fields = getattr(options, 'is_empty_fields', None)
     38        self.is_empty_exclude = getattr(options, 'is_empty_exclude', None)
     39        self.fieldset_attrs = getattr(options, 'fieldset_attrs', None)
     40        self.fieldset_legend = getattr(options, 'fieldset_legend', None)
     41        self.num_extra = getattr(options, 'num_extra', 1)
     42        self.orderable = getattr(options, 'orderable', False)
     43        self.output_type = getattr(options, 'output_type', 'tr')
     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)
     49        metaclassing.create_form_if_not_exists(new_class)
     50        metaclassing.check_no_fieldsets_in_form(new_class)
     51        return new_class
     52
     53class BaseFormSet(StrAndUnicode):
     54    """A collection of instances of the same Form class."""
     55
     56    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
     57        self.is_bound = data is not None or files is not None
     58        self.data = data
     59        self.files = files
     60        self.auto_id = auto_id
     61        self.prefix = prefix or 'form'
     62        self.initial = initial
     63        self._is_valid = None # Stores validation state after full_clean() has been called.
     64        self._create_forms()
     65
     66    def _create_forms(self):
     67        # TODO START - replace this ugly hack
     68        for field in self._meta.form.base_fields.values():
     69            if hasattr(field, 'queryset'):
     70                if field.cache_choices is False:
     71                    field.cache_choices = True
     72                    field.queryset._result_cache = None
     73                    field.queryset = field.queryset
     74                    field.cache_choices =  False
     75        # TODO END - replace this ugly hack
     76        self._create_management_forms()
     77        self._create_change_forms()
     78        self._create_add_forms()
     79        self.forms = self.change_forms + self.add_forms
     80
     81    def _create_management_forms(self):
     82        # Initialization is different depending on whether we recieved data, initial, or nothing.
     83        self.management_form = None
     84        if self.data or self.files:
     85            self.management_form = ManagementForm(self.data, self.files, auto_id=self.auto_id, prefix=self.prefix)
     86            if self.management_form.is_valid():
     87                self.change_forms_count = self.management_form.cleaned_data[CHANGE_FORMS_COUNT_FIELD_NAME]
     88                self.all_forms_count = self.management_form.cleaned_data[ALL_FORMS_COUNT_FIELD_NAME]
     89            else:
     90                # ManagementForm data is missing or has been tampered with."
     91                self.management_form = None
     92        if not self.management_form:
     93            self.change_forms_count = self.initial and len(self.initial) or 0
     94            self.all_forms_count = self.change_forms_count + self._meta.num_extra
     95            management_form_initial = {
     96                CHANGE_FORMS_COUNT_FIELD_NAME: self.change_forms_count,
     97                ALL_FORMS_COUNT_FIELD_NAME: self.all_forms_count,
     98            }
     99            self.management_form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial=management_form_initial)
     100
     101    def _create_change_forms(self):
     102        self.change_forms = []
     103        for i in range(0, self.change_forms_count):
     104            kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
     105            if self.data:
     106                kwargs['data'] = self.data
     107            if self.files:
     108                kwargs['files'] = self.files
     109            if self.initial:
     110                kwargs['initial'] = self.initial[i]
     111            change_form = self._meta.form(**kwargs)
     112            self.add_fields(change_form, i)
     113            self.change_forms.append(change_form)
     114
     115    def _create_add_forms(self):
     116        self.add_forms = []
     117        for i in range(self.change_forms_count, self.all_forms_count):
     118            kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
     119            if self.data:
     120                kwargs['data'] = self.data
     121            if self.files:
     122                kwargs['files'] = self.files
     123            add_form = self._meta.form(**kwargs)
     124            self.add_fields(add_form, i)
     125            self.add_forms.append(add_form)
     126
     127    def __unicode__(self):
     128        return getattr(self, 'as_%s' % self._meta.output_type)()
     129
     130    def add_prefix(self, index):
     131        return '%s-%s' % (self.prefix, index)
     132
     133    def add_fields(self, form, index):
     134        """A hook for adding extra fields on to each form instance."""
     135        if self._meta.orderable:
     136            form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_('Order'), initial=index+1)
     137        if self._meta.deletable:
     138            form.fields[DELETION_FIELD_NAME] = BooleanField(label=_('Delete'), required=False)
     139
     140    def as_table(self):
     141        "Returns this form rendered as HTML <tr>s."
     142        return u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms)
     143
     144    def as_ul(self):
     145        "Returns this form rendered as HTML <li>s."
     146        return u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms)
     147
     148    def as_p(self):
     149        "Returns this form rendered as HTML <p>s."
     150        return u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms)
     151
     152    def as_tr(self):
     153        "Returns this form rendered as HTML <td>s."
     154        output = [self.management_form.as_tr()]
     155        if self.forms:
     156            output.append(u'<tr>')
     157            output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden)
     158            output.append(u'</tr>')
     159        output.extend(form.as_tr() for form in self.forms)
     160        return '\n'.join(output)
     161
     162    def is_valid(self):
     163        """
     164        Returns True if the formset (and its forms) have no errors.
     165        """
     166        if self._is_valid is None:
     167            self.full_clean()
     168        return self._is_valid
     169
     170    def _get_errors(self):
     171        """
     172        Returns list of ErrorDict for all forms.
     173        """
     174        if self._is_valid is None:
     175            self.full_clean()
     176        return self._errors
     177    errors = property(_get_errors)
     178
     179    def _get_non_form_errors(self):
     180        """
     181        Returns an ErrorList of errors that aren't associated with a particular
     182        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
     183        are none.
     184        """
     185        if self._is_valid is None:
     186            self.full_clean()
     187        return self._non_form_errors
     188    non_form_errors = property(_get_non_form_errors)
     189
     190    def full_clean(self):
     191        """
     192        Cleans all of self.data and populates self._is_valid, self._errors,
     193        self._no_form_errors, self.cleaned_data and self.deleted_data.
     194        """
     195        self._is_valid = True # Assume the formset is valid until proven otherwise.
     196        self._errors = []
     197        self._non_form_errors = self._meta.error_class()
     198        if not self.is_bound: # Stop further processing.
     199            return
     200        self.cleaned_data = []
     201        self.deleted_data = []
     202        # Process change forms.
     203        for form in self.change_forms:
     204            if form.is_valid():
     205                if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     206                    self.deleted_data.append(form.cleaned_data)
     207                else:
     208                    self.cleaned_data.append(form.cleaned_data)
     209            else:
     210                self._is_valid = False
     211            self._errors.append(form.errors)
     212        # Process add forms in reverse so we can easily tell when the remaining
     213        # ones should be required.
     214        remaining_forms_required = False
     215        add_errors = []
     216        for i in range(len(self.add_forms)-1, -1, -1):
     217            form = self.add_forms[i]
     218            # If an add form is empty, reset it so it won't have any errors.
     219            if not remaining_forms_required and self.is_empty(form):
     220                form.reset()
     221                continue
     222            else:
     223                remaining_forms_required = True
     224                if form.is_valid():
     225                    self.cleaned_data.append(form.cleaned_data)
     226                else:
     227                    self._is_valid = False
     228            add_errors.append(form.errors)
     229        add_errors.reverse()
     230        self._errors.extend(add_errors)
     231        # Sort cleaned_data if the formset is orderable.
     232        if self._meta.orderable:
     233            self.cleaned_data.sort(lambda x, y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
     234        # Give self.clean() a chance to do validation.
     235        try:
     236            self.cleaned_data = self.clean()
     237        except ValidationError, e:
     238            self._non_form_errors = self._meta.error_class(e.messages)
     239            self._is_valid = False
     240        # If there were errors, remove the cleaned_data and deleted_data attributes.
     241        if not self._is_valid:
     242            delattr(self, 'cleaned_data')
     243            delattr(self, 'deleted_data')
     244
     245    def clean(self):
     246        """
     247        Hook for doing any extra formset-wide cleaning after Form.clean() has
     248        been called on every form. Any ValidationError raised by this method
     249        will not be associated with a particular form; it will be accesible
     250        via formset.non_form_errors()
     251        """
     252        return self.cleaned_data
     253
     254    def is_empty(self, form=None):
     255        """
     256        Returns True if the formset (including forms) or selected form
     257        is empty. Otherwise, False.
     258        """
     259        fields = self._meta.is_empty_fields and list(self._meta.is_empty_fields) or []
     260        exclude = self._meta.is_empty_exclude and list(self._meta.is_empty_exclude) or []
     261        if self._meta.orderable:
     262            exclude.append(ORDERING_FIELD_NAME)
     263        if form is None:
     264            for form in self:
     265                if not form.is_empty(fields, exclude):
     266                    return False
     267            return True
     268        else:
     269            return form.is_empty(fields, exclude)
     270
     271    def reset(self):
     272        """
     273        Resets the formset (including forms) to the state it was in
     274        before data was passed to it.
     275        """
     276        self.is_bound = False
     277        self.data = {}
     278        self.files = {}
     279        self._is_valid = None
     280        self._create_forms()
     281
     282    def is_multipart(self):
     283        """
     284        Returns True if the formset needs to be multipart-encrypted, i.e. its
     285        form has FileInput. Otherwise, False.
     286        """
     287        if self.forms:
     288            return self.forms[0].is_multipart()
     289        else:
     290            return False
     291
     292class FormSet(BaseFormSet):
     293    __metaclass__ = FormSetMetaclass
     294    _options = FormSetOptions
  • django/newforms/__init__.py

    === modified file 'django/newforms/__init__.py'
     
    1515from fields import *
    1616from forms import *
    1717from models import *
     18from formsets import *
  • django/newforms/forms.py

    === modified file 'django/newforms/forms.py'
     
    2929        self.fieldsets = getattr(options, 'fieldsets', None)
    3030        self.fields = getattr(options, 'fields', None)
    3131        self.exclude = getattr(options, 'exclude', None)
     32        self.inlines = getattr(options, 'inlines', None)
    3233        # other options
    3334        self.error_class = getattr(options, 'error_class', ErrorList)
    3435        self.error_row_class = getattr(options, 'error_row_class', 'error')
    3536        self.hidden_row_class = getattr(options, 'hidden_row_class', 'hidden')
    3637        self.label_capfirst = getattr(options, 'label_capfirst', True)
    3738        self.label_suffix = getattr(options, 'label_suffix', ':')
     39        self.output_type = getattr(options, 'output_type', 'table')
    3840        self.required_row_class = getattr(options, 'required_row_class', 'required')
    3941        self.validation_order = getattr(options, 'validation_order', None)
    4042
     
    4547        metaclassing.create_declared_fields(new_class)
    4648        metaclassing.create_base_fields_pool_from_declared_fields(new_class)
    4749        metaclassing.create_base_fields_from_base_fields_pool(new_class)
     50        metaclassing.create_fieldsets_if_inlines_exist(new_class)
    4851        return new_class
    4952
    5053class BaseForm(StrAndUnicode):
     
    5255    # class is different than Form. See the comments by the Form class for more
    5356    # information. Any improvements to the form API should be made to *this*
    5457    # class, not to the Form class.
    55     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
     58    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, inlines=None):
    5659        self.is_bound = data is not None or files is not None
    5760        self.data = data or {}
    5861        self.files = files or {}
    5962        self.auto_id = auto_id
    6063        self.prefix = prefix
    6164        self.initial = initial or {}
    62         self._errors = None # Stores the errors after clean() has been called.
     65        self.inlines = inlines or [] # It is not responsibility of this class to create inlines.
     66        self._is_valid = None # Stores validation state after full_clean() has been called.
    6367
    6468        # The base_fields class attribute is the *class-wide* definition of
    6569        # fields. Because a particular *instance* of the class might want to
     
    6973        self.fields = deepcopy(self.base_fields)
    7074
    7175    def __unicode__(self):
    72         return self.as_table()
     76        return getattr(self, 'as_%s' % self._meta.output_type)()
    7377
    7478    def __iter__(self):
    7579        for name, field in self.fields.items():
     
    8387            raise KeyError('Key %r not found in Form' % name)
    8488        return BoundField(self, field, name)
    8589
    86     def _get_errors(self):
    87         "Returns an ErrorDict for the data provided for the form."
    88         if self._errors is None:
    89             self.full_clean()
    90         return self._errors
    91     errors = property(_get_errors)
    92 
    93     def is_valid(self):
    94         """
    95         Returns True if the form has no errors. Otherwise, False. If errors are
    96         being ignored, returns False.
    97         """
    98         return self.is_bound and not bool(self.errors)
    99 
    10090    def has_fieldsets(self):
    10191        "Returns True if this form has fieldsets."
    10292        return bool(self._meta.fieldsets)
     
    183173            output.append(fieldset_end_html)
    184174        return u'\n'.join(output)
    185175
     176    def _inline_html_output(self, fields, inline, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html):
     177        "Helper function for outputting HTML from a inline. Used by _html_output."
     178        output = []
     179        if not is_first:
     180            legend_tag = attrs = u''
     181            if inline._meta.fieldset_legend:
     182                legend_tag = legend_tag_html % {
     183                    'legend': conditional_escape(force_unicode(inline._meta.fieldset_legend)),
     184                }
     185            if inline._meta.fieldset_attrs:
     186                attrs = flatatt(inline._meta.fieldset_attrs)
     187            output.append(fieldset_start_html % {
     188                'legend_tag': legend_tag,
     189                'attrs': attrs,
     190            })
     191        output.append(unicode(inline))
     192        if not is_last:
     193            output.append(fieldset_end_html)
     194        return u'\n'.join(output)
     195
    186196    def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html):
    187197        "Helper function for outputting HTML from a hidden fields. Used by _html_output."
    188198        if self._meta.hidden_row_class:
     
    210220        if top_errors:
    211221            output.append(self._top_errors_html_output(top_errors, top_errors_html))
    212222        if self.has_fieldsets():
    213             for i, fieldset in enumerate(self._meta.fieldsets):
    214                 fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
     223            inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines.
     224            for i, fieldset_or_inline in enumerate(self._meta.fieldsets):
    215225                is_first = (i == 0)
    216226                is_last = (i + 1 == len(self._meta.fieldsets))
    217                 output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
    218                     fieldset_start_html, fieldset_end_html, legend_tag_html))
     227                if isinstance(fieldset_or_inline, dict):
     228                    fields = dict((name, visible_fields[name]) for name in fieldset_or_inline['fields'] if name in visible_fields)
     229                    output.append(self._fieldset_html_output(fields, fieldset_or_inline, is_first, is_last,
     230                        fieldset_start_html, fieldset_end_html, legend_tag_html))
     231                else:
     232                    output.append(self._inline_html_output(fields, inlines.pop(0), is_first, is_last,
     233                        fieldset_start_html, fieldset_end_html, legend_tag_html))
    219234        else:
    220235            for name in self.fields:
    221236                if name in visible_fields:
     
    266281        }
    267282        return self._html_output(**kwargs)
    268283
     284    def as_tr(self):
     285        "Returns this form rendered as HTML <td>s."
     286        if self.has_fieldsets():
     287            raise ValueError("%s has fieldsets so its method as_tr cannot be used." % self.__class__.__name__)
     288        colspan = len([bf for bf in self if not bf.is_hidden])
     289        kwargs = {
     290            'row_html': u'<td%(attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>',
     291            'label_tag_html': u'',
     292            'help_text_html': u' %(help_text)s',
     293            'top_errors_html': u'<tr><td colspan="%s">%%(top_errors)s</td></tr>\n<tr>' % colspan,
     294            'fieldset_start_html': u'',
     295            'fieldset_end_html': u'',
     296            'legend_tag_html': u'',
     297            'hidden_fields_html': u'</tr>\n<tr%%(attrs)s><td colspan="%s">%%(hidden_fields)s</td></tr>' % colspan,
     298        }
     299        html_output = self._html_output(**kwargs)
     300        if not html_output.startswith('<tr>'):
     301            html_output = u'<tr>\n%s' % html_output
     302        if not html_output.endswith('</tr>'):
     303            html_output = u'%s\n</tr>' % html_output
     304        return html_output
     305
     306    def is_valid(self):
     307        """
     308        Returns True if the form and its inlines have no errors.
     309        """
     310        if self._is_valid is None:
     311            self.full_clean()
     312        return self._is_valid
     313
     314    def _get_errors(self):
     315        "Returns an ErrorDict for the data provided for the form."
     316        if self._is_valid is None:
     317            self.full_clean()
     318        return self._errors
     319    errors = property(_get_errors)
     320
    269321    def non_field_errors(self):
    270322        """
    271323        Returns an ErrorList of errors that aren't associated with a particular
     
    276328
    277329    def full_clean(self):
    278330        """
    279         Cleans all of self.data and populates self._errors and
     331        Cleans all of self.data and populates self._is_valid, self._errors and
    280332        self.cleaned_data.
    281333        """
     334        self._is_valid = True # Assume the form is valid until proven otherwise.
    282335        self._errors = ErrorDict()
    283336        if not self.is_bound: # Stop further processing.
    284337            return
    285338        self.cleaned_data = {}
     339        # Process fields.
    286340        if self._meta.validation_order:
    287341            items = [(name, self.fields[name]) for name in self._meta.validation_order]
    288342        else:
    289343            items = self.fields.items()
    290         for name, field in items:
    291             # value_from_datadict() gets the data from the data dictionaries.
    292             # Each widget type knows how to retrieve its own data, because some
    293             # widgets split data over several HTML fields.
    294             value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
     344        for bf in self:
    295345            try:
    296                 if isinstance(field, FileField):
    297                     initial = self.initial.get(name, field.initial)
    298                     value = field.clean(value, initial)
     346                if isinstance(bf.field, FileField):
     347                    initial = self.initial.get(bf.name, bf.field.initial)
     348                    value = bf.field.clean(bf.data, initial)
    299349                else:
    300                     value = field.clean(value)
    301                 self.cleaned_data[name] = value
    302                 if hasattr(self, 'clean_%s' % name):
    303                     value = getattr(self, 'clean_%s' % name)()
    304                     self.cleaned_data[name] = value
     350                    value = bf.field.clean(bf.data)
     351                self.cleaned_data[bf.name] = value
     352                if hasattr(self, 'clean_%s' % bf.name):
     353                    self.cleaned_data[bf.name] = getattr(self, 'clean_%s' % bf.name)()
    305354            except ValidationError, e:
    306                 self._errors[name] = self._meta.error_class(e.messages)
    307                 if name in self.cleaned_data:
    308                     del self.cleaned_data[name]
     355                self._errors[bf.name] = self._meta.error_class(e.messages)
     356                self._is_valid = False
     357                if bf.name in self.cleaned_data:
     358                    del self.cleaned_data[bf.name]
     359        # Process inlines.
     360        for inline in self.inlines:
     361            inline.full_clean()
     362            if not inline.is_valid():
     363                self._is_valid = False
     364        # Give self.clean() a chance to do validation.
    309365        try:
    310366            self.cleaned_data = self.clean()
    311367        except ValidationError, e:
    312368            self._errors[NON_FIELD_ERRORS] = self._meta.error_class(e.messages)
    313         if self._errors:
     369            self._is_valid = False
     370        # If there were errors, remove the cleaned_data attribute.
     371        if not self._is_valid:
    314372            delattr(self, 'cleaned_data')
    315373
    316374    def clean(self):
     
    322380        """
    323381        return self.cleaned_data
    324382
     383    def is_empty(self, fields=None, exclude=None):
     384        """
     385        Returns True if the form (including inlines) is empty. Otherwise, False.
     386        """
     387        for bf in self:
     388            if fields and bf.name not in fields:
     389                continue
     390            if exclude and bf.name in exclude:
     391                continue
     392            if not bf.widget.is_empty(bf.data):
     393                return False
     394        for inline in self.inlines:
     395            if not inline.is_empty():
     396                return False
     397        return True
     398
     399    def reset(self):
     400        """
     401        Resets the form (including inlines) to the state it was in
     402        before data was passed to it.
     403        """
     404        self.is_bound = False
     405        self.data = {}
     406        self.files = {}
     407        self._is_valid = None
     408        for inline in self.inlines:
     409            inline.reset()
     410
    325411    def is_multipart(self):
    326412        """
    327         Returns True if the form needs to be multipart-encrypted, i.e. it has
    328         FileInput. Otherwise, False.
     413        Returns True if the form (including inlines) needs to be
     414        multipart-encrypted, i.e. it has FileInput. Otherwise, False.
    329415        """
    330416        for field in self.fields.values():
    331417            if field.widget.needs_multipart_form:
    332418                return True
     419        for inline in self.inlines:
     420            if inline.is_multipart():
     421                return True
    333422        return False
    334423
    335424class Form(BaseForm):
  • django/newforms/metaclassing.py

    === modified file 'django/newforms/metaclassing.py'
     
    5050        raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__)
    5151    if cls._meta.fieldsets:
    5252        names = []
    53         for fieldset in cls._meta.fieldsets:
    54             names.extend(fieldset['fields'])
     53        for fieldset_or_inline in cls._meta.fieldsets:
     54            if isinstance(fieldset_or_inline, dict):
     55                names.extend(fieldset_or_inline['fields'])
    5556    elif cls._meta.fields:
    5657        names = cls._meta.fields
    5758    elif cls._meta.exclude:
     
    5960    else:
    6061        names = cls._base_fields_pool.keys()
    6162    cls.base_fields = SortedDict([(name, cls._base_fields_pool[name]) for name in names])
     63
     64def create_fieldsets_if_inlines_exist(cls):
     65    if cls._meta.inlines is not None:
     66        if cls._meta.fieldsets is not None:
     67            raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__)
     68        cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines)
     69
     70def create_form_if_not_exists(cls):
     71    if not cls._meta.form:
     72        form_attrs = {
     73            'Meta': type('Meta', (), cls._meta.__dict__),
     74        }
     75        for name, attr in cls.__dict__.items():
     76            if isinstance(attr, Field):
     77                form_attrs[name] = attr
     78                delattr(cls, name)
     79        cls._meta.form = type('%sForm' % cls.__name__, (cls._meta.base_form,), form_attrs)
     80
     81def check_no_fieldsets_in_form(cls):
     82    if cls._meta.form._meta.fieldsets:
     83        raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__)
     84
     85def add_fk_attribute_and_remove_fk_from_base_fields(cls):
     86    # Get some options - if models are not set, this class would not be used directly.
     87    parent_model, model, fk_name = cls._meta.parent_model, cls._meta.model, cls._meta.fk_name
     88    if not (parent_model and model):
     89        return
     90    # Try to discover what the foreign key from model to parent_model is.
     91    fks_to_parent = []
     92    for field in model._meta.fields:
     93        # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy.
     94        try:
     95            if field.rel.to == parent_model:
     96                fks_to_parent.append(field)
     97        except AttributeError:
     98            pass
     99    if len(fks_to_parent) == 0:
     100        raise ImproperlyConfigured("%s has no ForeignKey to %s." % (model, parent_model))
     101    if fk_name:
     102        fks_to_parent = [fk for fk in fks_to_parent if fk.name == fk_name]
     103    if len(fks_to_parent) > 1:
     104        raise ImproperlyConfigured("%s has more than one ForeignKey to %s." % (model, parent_model))
     105    cls.fk = fks_to_parent[0]
     106    # Try to remove the foreign key from base_fields to keep it transparent to the form.
     107    try:
     108        del cls._meta.form.base_fields[cls.fk.name]
     109    except KeyError:
     110        pass
  • django/newforms/models.py

    === modified file 'django/newforms/models.py'
     
    1111
    1212from util import ValidationError
    1313from forms import FormOptions, FormMetaclass, BaseForm
    14 from fields import Field, ChoiceField, EMPTY_VALUES
     14from formsets import FormSetOptions, FormSetMetaclass, BaseFormSet, DELETION_FIELD_NAME
     15from fields import Field, ChoiceField, EMPTY_VALUES, IntegerField, HiddenInput
    1516from widgets import Select, SelectMultiple, MultipleHiddenInput
    1617import metaclassing
    1718
    1819__all__ = (
    1920    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    2021    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
    21     'ModelChoiceField', 'ModelMultipleChoiceField'
     22    'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelFormSet',
     23    'ModelFormSet', 'BaseInlineFormSet', 'InlineFormSet',
    2224)
    2325
    2426def save_instance(form, instance, fields=None, fail_message='saved',
     
    218220        metaclassing.create_declared_fields(new_class)
    219221        metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class)
    220222        metaclassing.create_base_fields_from_base_fields_pool(new_class)
     223        metaclassing.create_fieldsets_if_inlines_exist(new_class)
    221224        return new_class
    222225
    223226class BaseModelForm(BaseForm):
    224227    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, instance=None):
    225         opts = self._meta
    226228        if instance is None:
    227             # if we didn't get an instance, instantiate a new one
    228             self.instance = opts.model()
     229            # If we didn't get an instance, instantiate a new one.
     230            self.instance = self._meta.model()
    229231            object_data = {}
    230232        else:
    231233            self.instance = instance
    232234            object_data = model_to_dict(instance, self.base_fields.keys())
    233         # if initial was provided, it should override the values from instance
     235        # If initial was provided, it should override the values from instance.
    234236        if initial is not None:
    235237            object_data.update(initial)
    236         BaseForm.__init__(self, data, files, auto_id, prefix, object_data)
     238        # Create inlines.
     239        inlines = []
     240        if self.has_fieldsets():
     241            for fieldset_or_inline in self._meta.fieldsets:
     242                if not isinstance(fieldset_or_inline, dict):
     243                    inline_prefix = '%s-%s' % (prefix or 'inline', len(inlines))
     244                    inlines.append(fieldset_or_inline(data, files, auto_id, inline_prefix, self.instance))
     245        BaseForm.__init__(self, data, files, auto_id, prefix, object_data, inlines)
    237246
    238247    def save(self, commit=True):
    239248        """
     
    247256            fail_message = 'created'
    248257        else:
    249258            fail_message = 'changed'
    250         return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
     259        self.saved_instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
     260        self.saved_inline_instances = [inline.save(commit) for inline in self.inlines]
     261        return self.saved_instance
    251262
    252263class ModelForm(BaseModelForm):
    253264    __metaclass__ = ModelFormMetaclass
    254265    _options = ModelFormOptions
    255266
    256 
    257267# Fields #####################################################################
    258268
    259269class QuerySetIterator(object):
     
    364374            else:
    365375                final_values.append(obj)
    366376        return final_values
     377
     378# Model-FormSet integration ###################################################
     379
     380class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
     381    def __init__(self, options=None):
     382        super(ModelFormSetOptions, self).__init__(options)
     383        # other default options
     384        self.base_form = getattr(options, 'base_form', ModelForm)
     385
     386class BaseModelFormSet(BaseFormSet):
     387    """
     388    A ``FormSet`` for editing a queryset and/or adding new objects to it.
     389    """
     390
     391    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None, **kwargs):
     392        if queryset is None:
     393            self.queryset = self.get_queryset(**kwargs)
     394        else:
     395            self.queryset = queryset
     396        self._pk_name = self._meta.model._meta.pk.name
     397        self._pks = [obj.pk for obj in self.queryset]
     398        initial = [model_to_dict(obj, self._meta.form.base_fields.keys()) for obj in self.queryset]
     399        super(BaseModelFormSet, self).__init__(data, files, auto_id, prefix, initial)
     400
     401    def get_queryset(self, **kwargs):
     402        """
     403        Hook to returning a queryset for this model.
     404        """
     405        return self._meta.form._meta.model._default_manager.all()
     406
     407    def add_fields(self, form, index):
     408        """Add a hidden field for the object's primary key."""
     409        pk = index < len(self._pks) and self._pks[index] or None
     410        form.fields[self._pk_name] = IntegerField(required=False, widget=HiddenInput, initial=pk)
     411        super(BaseModelFormSet, self).add_fields(form, index)
     412
     413    def save_new(self, form, commit=True):
     414        """Saves and returns a new model instance for the given form."""
     415        return save_instance(form, self._meta.form._meta.model(), commit=commit)
     416
     417    def save_instance(self, form, instance, commit=True):
     418        """Saves and returns an existing model instance for the given form."""
     419        return save_instance(form, instance, commit=commit)
     420
     421    def save(self, commit=True):
     422        """Saves model instances for every form, adding and changing instances
     423        as necessary, and returns the list of instances.
     424        """
     425        return self.save_existing_objects(commit) + self.save_new_objects(commit)
     426
     427    def save_existing_objects(self, commit=True):
     428        if not self.queryset:
     429            return []
     430        # Put the objects from self.queryset into a dict so they are easy to lookup by pk.
     431        existing_objects = {}
     432        for obj in self.queryset:
     433            existing_objects[obj.pk] = obj
     434        # If commit is not permitted, objects cannot be deleted so they are saved as an instance variable.
     435        if not commit:
     436            self.instances_to_delete = []
     437        saved_instances = []
     438        for form in self.change_forms:
     439            obj = existing_objects[form.cleaned_data[self._pk_name]]
     440            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     441                if commit:
     442                    obj.delete()
     443                else:
     444                    self.instances_to_delete.append(obj)
     445            else:
     446                saved_instances.append(self.save_instance(form, obj, commit=commit))
     447        return saved_instances
     448
     449    def save_new_objects(self, commit=True):
     450        new_objects = []
     451        for form in self.add_forms:
     452            if form.is_empty():
     453                continue
     454            # If someone has marked an add form for deletion, don't save the object.
     455            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     456                continue
     457            new_objects.append(self.save_new(form, commit=commit))
     458        return new_objects
     459
     460class ModelFormSet(BaseModelFormSet):
     461    _options =  ModelFormSetOptions
     462
     463class InlineFormSetOptions(ModelFormSetOptions):
     464    def __init__(self, options=None):
     465        super(InlineFormSetOptions, self).__init__(options)
     466        # new options
     467        self.parent_model = getattr(options, 'parent_model', None)
     468        self.fk_name = getattr(options, 'fk_name', None)
     469        # other default options
     470        self.deletable = getattr(options, 'deletable', True)
     471        self.num_extra = getattr(options, 'num_extra', 3)
     472
     473class InlineFormSetMetaclass(FormSetMetaclass):
     474    def __new__(cls, name, bases, attrs):
     475        new_class = type.__new__(cls, name, bases, attrs)
     476        metaclassing.create_meta(new_class)
     477        metaclassing.create_form_if_not_exists(new_class)
     478        metaclassing.check_no_fieldsets_in_form(new_class)
     479        metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class)
     480        return new_class
     481
     482class BaseInlineFormSet(BaseModelFormSet):
     483    """A formset for child objects related to a parent."""
     484
     485    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instance=None, **kwargs):
     486        self.instance = instance
     487        super(BaseInlineFormSet, self).__init__(data, files, auto_id, prefix, **kwargs)
     488
     489    def get_queryset(self, **kwargs):
     490        """
     491        Returns this FormSet's queryset, but restricted to children of
     492        self.instance
     493        """
     494        if self.instance is None:
     495            return self._meta.form._meta.model._default_manager.none()
     496        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     497        return queryset.filter(**{self.fk.name: self.instance})
     498
     499    def save_new(self, form, commit=True):
     500        kwargs = {self.fk.name: self.instance}
     501        new_obj = self._meta.form._meta.model(**kwargs)
     502        return save_instance(form, new_obj, commit=commit)
     503
     504class InlineFormSet(BaseInlineFormSet):
     505    __metaclass__ = InlineFormSetMetaclass
     506    _options = InlineFormSetOptions
  • django/newforms/widgets.py

    === modified file 'django/newforms/widgets.py'
     
    6969        """
    7070        return data.get(name, None)
    7171
     72    def is_empty(self, value):
     73        """
     74        Returns True if this form is empty.
     75        """
     76        if value not in (None, ''):
     77            return False
     78        return True
     79
    7280    def id_for_label(self, id_):
    7381        """
    7482        Returns the HTML ID attribute of this Widget for use by a <label>,
     
    206214            return False
    207215        return super(CheckboxInput, self).value_from_datadict(data, files, name)
    208216
     217    def is_empty(self, value):
     218        # This widget will always either be True or False, so always return the
     219        # opposite value so False values will make the form empty.
     220        return not value
     221
    209222class Select(Widget):
    210223    def __init__(self, attrs=None, row_attrs=None, choices=()):
    211224        super(Select, self).__init__(attrs, row_attrs)
     
    248261        value = data.get(name, None)
    249262        return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
    250263
     264    def is_empty(self, value):
     265        # This widget will always either be True or False, so always return the
     266        # opposite value so False values will make the form empty.
     267        return not value
     268
    251269class SelectMultiple(Select):
    252270    def render(self, name, value, attrs=None, choices=()):
    253271        if value is None: value = []
     
    439457    def value_from_datadict(self, data, files, name):
    440458        return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
    441459
     460    def is_empty(self, value):
     461        for widget, val in zip(self.widgets, value):
     462            if not widget.is_empty(val):
     463                return False
     464        return True
     465
    442466    def format_output(self, rendered_widgets):
    443467        """
    444468        Given a list of rendered widgets (as strings), returns a Unicode string
Back to Top