Ticket #6632: 02-newforms-inlines.diff

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

    === added file 'django/newforms/formsets.py'
     
     1from django.core.exceptions import ImproperlyConfigured
     2from django.utils.datastructures import InheritableOptions
     3from django.utils.encoding import StrAndUnicode
     4from django.utils.translation import ugettext as _
     5
     6from fields import Field, BooleanField, IntegerField
     7from forms import Form, FormOptions
     8from widgets import HiddenInput
     9from util import ValidationError
     10
     11__all__ = ('BaseFormSet', 'FormSet')
     12
     13# Special field names.
     14CHANGE_FORMS_COUNT_FIELD_NAME = 'CHANGE-FORMS-COUNT'
     15ALL_FORMS_COUNT_FIELD_NAME = 'ALL-FORMS-COUNT'
     16ORDERING_FIELD_NAME = 'ORDER'
     17DELETION_FIELD_NAME = 'DELETE'
     18
     19class ManagementForm(Form):
     20    """
     21    ``ManagementForm`` is used to keep track of how many form instances
     22    are displayed on the page. If adding new forms via javascript, you should
     23    increment the count field of this form as well.
     24    """
     25    def __init__(self, *args, **kwargs):
     26        self.base_fields[CHANGE_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
     27        self.base_fields[ALL_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
     28        super(ManagementForm, self).__init__(*args, **kwargs)
     29
     30class FormSetOptions(InheritableOptions):
     31    _formset_options = {
     32        # form
     33        'form': None,
     34        'base_form': Form,
     35        # other options
     36        'deletable': False,
     37        'emptiness_test_fields': None,
     38        'emptiness_test_exclude': None,
     39        'fieldset_attrs': {},
     40        'fieldset_html_output_method': None,
     41        'fieldset_legend': None,
     42        'num_extra': 1,
     43        'orderable': False,
     44        'output_type': 'tr',
     45    }
     46    _default_options = FormOptions._default_options.copy()
     47    _default_options.update(_formset_options)
     48
     49class FormSetMetaclass(type):
     50
     51    def create_options(cls, new_cls):
     52        new_cls._meta = new_cls.options(new_cls)
     53        try:
     54            delattr(new_cls, 'Meta')
     55        except AttributeError:
     56            pass
     57    create_options = classmethod(create_options)
     58
     59    def create_form_if_not_exists(cls, new_cls):
     60        if not new_cls._meta.form:
     61            form_attrs = {
     62                'Meta': type('Meta', (), new_cls._meta.__dict__),
     63            }
     64            for name, attr in new_cls.__dict__.items():
     65                if isinstance(attr, Field):
     66                    form_attrs[name] = attr
     67                    delattr(new_cls, name)
     68            new_cls._meta.form = type('%sForm' % new_cls.__name__, (new_cls._meta.base_form,), form_attrs)
     69    create_form_if_not_exists = classmethod(create_form_if_not_exists)
     70
     71    def check_no_fieldsets_in_form(cls, new_cls):
     72        if new_cls._meta.form._meta.fieldsets is not None:
     73            raise ImproperlyConfigured("%s cannot have fieldsets." % new_cls.__name__)
     74    check_no_fieldsets_in_form = classmethod(check_no_fieldsets_in_form)
     75
     76    def __new__(cls, name, bases, attrs):
     77        new_cls = type.__new__(cls, name, bases, attrs)
     78        cls.create_options(new_cls)
     79        cls.create_form_if_not_exists(new_cls)
     80        cls.check_no_fieldsets_in_form(new_cls)
     81        return new_cls
     82
     83class BaseFormSet(StrAndUnicode):
     84    """A collection of instances of the same Form class."""
     85
     86    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
     87        self.is_bound = data is not None or files is not None
     88        self.data = data
     89        self.files = files
     90        self.auto_id = auto_id
     91        self.prefix = prefix or 'form'
     92        self.initial = initial
     93        self._is_valid = None # Stores validation state after full_clean() has been called.
     94        self._create_forms()
     95
     96    def _create_forms(self):
     97        self._create_management_forms()
     98        self._create_change_forms()
     99        self._create_add_forms()
     100        self.forms = self.change_forms + self.add_forms
     101
     102    def _create_management_forms(self):
     103        # Initialization is different depending on whether we recieved data, initial, or nothing.
     104        self.management_form = None
     105        if self.data or self.files:
     106            self.management_form = ManagementForm(self.data, self.files, auto_id=self.auto_id, prefix=self.prefix)
     107            if self.management_form.is_valid():
     108                self.change_forms_count = self.management_form.cleaned_data[CHANGE_FORMS_COUNT_FIELD_NAME]
     109                self.all_forms_count = self.management_form.cleaned_data[ALL_FORMS_COUNT_FIELD_NAME]
     110            else:
     111                # ManagementForm data is missing or has been tampered with."
     112                self.management_form = None
     113        if not self.management_form:
     114            self.change_forms_count = self.initial and len(self.initial) or 0
     115            self.all_forms_count = self.change_forms_count + self._meta.num_extra
     116            management_form_initial = {
     117                CHANGE_FORMS_COUNT_FIELD_NAME: self.change_forms_count,
     118                ALL_FORMS_COUNT_FIELD_NAME: self.all_forms_count,
     119            }
     120            self.management_form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial=management_form_initial)
     121
     122    def _create_change_forms(self):
     123        self.change_forms = []
     124        for i in range(0, self.change_forms_count):
     125            kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
     126            if self.data:
     127                kwargs['data'] = self.data
     128            if self.files:
     129                kwargs['files'] = self.files
     130            if self.initial:
     131                kwargs['initial'] = self.initial[i]
     132            change_form = self._meta.form(**kwargs)
     133            self.add_fields(change_form, i)
     134            self.change_forms.append(change_form)
     135
     136    def _create_add_forms(self):
     137        self.add_forms = []
     138        for i in range(self.change_forms_count, self.all_forms_count):
     139            kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
     140            if self.data:
     141                kwargs['data'] = self.data
     142            if self.files:
     143                kwargs['files'] = self.files
     144            add_form = self._meta.form(**kwargs)
     145            self.add_fields(add_form, i)
     146            self.add_forms.append(add_form)
     147
     148    def __unicode__(self):
     149        return getattr(self, 'as_%s' % self._meta.output_type)()
     150
     151    def add_prefix(self, index):
     152        return '%s-%s' % (self.prefix, index)
     153
     154    def add_fields(self, form, index):
     155        """A hook for adding extra fields on to each form instance."""
     156        if self._meta.orderable:
     157            form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_('Order'), initial=index+1)
     158        if self._meta.deletable:
     159            form.fields[DELETION_FIELD_NAME] = BooleanField(label=_('Delete'), required=False)
     160
     161    def as_table(self):
     162        "Returns this form rendered as HTML <tr>s."
     163        return u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms)
     164
     165    def as_ul(self):
     166        "Returns this form rendered as HTML <li>s."
     167        return u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms)
     168
     169    def as_p(self):
     170        "Returns this form rendered as HTML <p>s."
     171        return u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms)
     172
     173    def as_tr(self):
     174        "Returns this form rendered as HTML <td>s."
     175        output = [self.management_form.as_tr()]
     176        if self.forms:
     177            output.append(u'<tr>')
     178            output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden)
     179            output.append(u'</tr>')
     180        output.extend(form.as_tr() for form in [self.management_form] + self.forms)
     181        return '\n'.join(output)
     182
     183    def is_valid(self):
     184        """
     185        Returns True if the formset (and its forms) have no errors.
     186        """
     187        if self._is_valid is None:
     188            self.full_clean()
     189        return self._is_valid
     190
     191    def _get_errors(self):
     192        """
     193        Returns list of ErrorDict for all forms.
     194        """
     195        if self._is_valid is None:
     196            self.full_clean()
     197        return self._errors
     198    errors = property(_get_errors)
     199
     200    def _get_non_form_errors(self):
     201        """
     202        Returns an ErrorList of errors that aren't associated with a particular
     203        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
     204        are none.
     205        """
     206        if self._is_valid is None:
     207            self.full_clean()
     208        return self._non_form_errors
     209    non_form_errors = property(_get_non_form_errors)
     210
     211    def full_clean(self):
     212        """
     213        Cleans all of self.data and populates self._is_valid, self._errors,
     214        self._no_form_errors, self.cleaned_data and self.deleted_data.
     215        """
     216        self._is_valid = True # Assume the formset is valid until proven otherwise.
     217        self._errors = []
     218        self._non_form_errors = self._meta.error_class()
     219        if not self.is_bound: # Stop further processing.
     220            return
     221        self.cleaned_data = []
     222        self.deleted_data = []
     223        # Process change forms.
     224        for form in self.change_forms:
     225            if form.is_valid():
     226                if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     227                    self.deleted_data.append(form.cleaned_data)
     228                else:
     229                    self.cleaned_data.append(form.cleaned_data)
     230            else:
     231                self._is_valid = False
     232            self._errors.append(form.errors)
     233        # Process add forms in reverse so we can easily tell when the remaining
     234        # ones should be required.
     235        remaining_forms_required = False
     236        add_errors = []
     237        for i in range(len(self.add_forms)-1, -1, -1):
     238            form = self.add_forms[i]
     239            # If an add form is empty, reset it so it won't have any errors.
     240            if not remaining_forms_required and self.is_empty(form):
     241                form.reset()
     242                continue
     243            else:
     244                remaining_forms_required = True
     245                if form.is_valid():
     246                    self.cleaned_data.append(form.cleaned_data)
     247                else:
     248                    self._is_valid = False
     249            add_errors.append(form.errors)
     250        add_errors.reverse()
     251        self._errors.extend(add_errors)
     252        # Sort cleaned_data if the formset is orderable.
     253        if self._meta.orderable:
     254            self.cleaned_data.sort(lambda x, y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
     255        # Give self.clean() a chance to do validation.
     256        try:
     257            self.cleaned_data = self.clean()
     258        except ValidationError, e:
     259            self._non_form_errors = self._meta.error_class(e.messages)
     260            self._is_valid = False
     261        # If there were errors, remove the cleaned_data and deleted_data attributes.
     262        if not self._is_valid:
     263            delattr(self, 'cleaned_data')
     264            delattr(self, 'deleted_data')
     265
     266    def clean(self):
     267        """
     268        Hook for doing any extra formset-wide cleaning after Form.clean() has
     269        been called on every form. Any ValidationError raised by this method
     270        will not be associated with a particular form; it will be accesible
     271        via formset.non_form_errors()
     272        """
     273        return self.cleaned_data
     274
     275    def is_empty(self, form=None):
     276        """
     277        Returns True if the formset (including forms) or selected form
     278        is empty. Otherwise, False.
     279        """
     280        fields = list(self._meta.emptiness_test_fields) or []
     281        exclude = list(self._meta.emptiness_test_exclude) or []
     282        if self._meta.orderable:
     283            exclude.append(ORDERING_FIELD_NAME)
     284        if form is None:
     285            for form in self:
     286                if not form.is_empty(fields, exclude):
     287                    return False
     288            return True
     289        else:
     290            return form.is_empty(fields, exclude)
     291
     292    def reset(self):
     293        """
     294        Resets the formset (including forms) to the state it was in
     295        before data was passed to it.
     296        """
     297        self.is_bound = False
     298        self.data = {}
     299        self.files = {}
     300        self._is_valid = None
     301        self._create_forms()
     302
     303    def is_multipart(self):
     304        """
     305        Returns True if the formset needs to be multipart-encrypted, i.e. its
     306        form has FileInput. Otherwise, False.
     307        """
     308        if self.forms:
     309            return self.forms[0].is_multipart()
     310        else:
     311            return False
     312
     313class FormSet(BaseFormSet):
     314    __metaclass__ = FormSetMetaclass
     315    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'
     
    44
    55from copy import deepcopy
    66
     7from django.core.exceptions import ImproperlyConfigured
    78from django.utils.datastructures import SortedDict, InheritableOptions
    89from django.utils.html import conditional_escape
    910from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
     
    2930        'fieldsets': None,
    3031        'fields': None,
    3132        'exclude': None,
     33        'inlines': None,
    3234        # other options
    3335        'error_class': ErrorList,
    3436        'formfield_for_formfield': lambda self, formfield: formfield,
     
    3739        'html_class_for_required_fields': 'required',
    3840        'label_capitalization': True,
    3941        'label_suffix': ':',
     42        'output_type': 'table',
    4043        'validation_order': None,
    4144    }
    4245
     
    7679    def create_base_fields_from_base_fields_pool(cls, new_cls):
    7780        if new_cls._meta.fieldsets:
    7881            names = []
    79             for fieldset in new_cls._meta.fieldsets:
    80                 names.extend(fieldset['fields'])
     82            for fieldset_or_inline in new_cls._meta.fieldsets:
     83                if isinstance(fieldset_or_inline, dict):
     84                    names.extend(fieldset_or_inline['fields'])
    8185        elif new_cls._meta.fields:
    8286            names = new_cls._meta.fields
    8387        elif new_cls._meta.exclude:
     
    8791        new_cls.base_fields = SortedDict([(name, new_cls._meta.formfield_for_formfield(new_cls._meta, new_cls._base_fields_pool[name])) for name in names])
    8892    create_base_fields_from_base_fields_pool = classmethod(create_base_fields_from_base_fields_pool)
    8993
     94    def create_fieldsets_if_inlines_exist(cls, new_cls):
     95        if new_cls._meta.inlines is not None:
     96            if new_cls._meta.fieldsets is not None:
     97                raise ImproperlyConfigured("Options fieldsets and inlines cannot be used together.")
     98            new_cls._meta.fieldsets = [{'fields': new_cls.base_fields.keys()}] + list(new_cls._meta.inlines)
     99    create_fieldsets_if_inlines_exist = classmethod(create_fieldsets_if_inlines_exist)
     100
    90101    def __new__(cls, name, bases, attrs):
    91102        new_cls = type.__new__(cls, name, bases, attrs)
    92103        cls.create_options(new_cls)
    93104        cls.create_declared_fields(new_cls)
    94105        cls.create_base_fields_pool_from_declared_fields(new_cls)
    95106        cls.create_base_fields_from_base_fields_pool(new_cls)
     107        cls.create_fieldsets_if_inlines_exist(new_cls)
    96108        return new_cls
    97109
    98110class BaseForm(StrAndUnicode):
     
    100112    # class is different than Form. See the comments by the Form class for more
    101113    # information. Any improvements to the form API should be made to *this*
    102114    # class, not to the Form class.
    103     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
     115    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, inlines=None):
    104116        self.is_bound = data is not None or files is not None
    105117        self.data = data or {}
    106118        self.files = files or {}
    107119        self.auto_id = auto_id
    108120        self.prefix = prefix
    109121        self.initial = initial or {}
    110         self._errors = None # Stores the errors after clean() has been called.
     122        self.inlines = inlines or [] # It is not responsibility of this class to create inlines.
     123        self._is_valid = None # Stores validation state after full_clean() has been called.
    111124
    112125        # The base_fields class attribute is the *class-wide* definition of
    113126        # fields. Because a particular *instance* of the class might want to
     
    117130        self.fields = deepcopy(self.base_fields)
    118131
    119132    def __unicode__(self):
    120         return self.as_table()
     133        return getattr(self, 'as_%s' % self._meta.output_type)()
    121134
    122135    def __iter__(self):
    123136        for name, field in self.fields.items():
     
    131144            raise KeyError('Key %r not found in Form' % name)
    132145        return BoundField(self, field, name)
    133146
    134     def _get_errors(self):
    135         "Returns an ErrorDict for the data provided for the form."
    136         if self._errors is None:
    137             self.full_clean()
    138         return self._errors
    139     errors = property(_get_errors)
    140 
    141     def is_valid(self):
    142         """
    143         Returns True if the form has no errors. Otherwise, False. If errors are
    144         being ignored, returns False.
    145         """
    146         return self.is_bound and not bool(self.errors)
    147 
    148147    def has_fieldsets(self):
    149148        "Returns True if this form has fieldsets."
    150149        return bool(self._meta.fieldsets)
     
    192191            output.append(fieldset_end % u'</fieldset>')
    193192        return u'\n'.join(output)
    194193
     194    def inline_html_output(self, inline, fieldset_start, fieldset_end, is_first, is_last):
     195        "Helper function for outputting HTML from a inline. Used by _html_output."
     196        output = []
     197        if fieldset_start and not is_first:
     198            fieldset_attrs = flatatt(inline._meta.fieldset_attrs)
     199            if inline._meta.fieldset_legend:
     200                legend_tag = u'\n<legend>%s</legend>' % conditional_escape(force_unicode(inline._meta.fieldset_legend))
     201            else:
     202                legend_tag = u''
     203            output.append(fieldset_start % (u'<fieldset%s>%s' % (fieldset_attrs, legend_tag)))
     204        output.append(unicode(inline))
     205        if fieldset_end and not is_last:
     206            output.append(fieldset_end % u'</fieldset>')
     207        return u'\n'.join(output)
     208
    195209    def hidden_fields_html_output(self, hidden_fields, hidden_fields_row):
    196210        "Helper function for outputting HTML from a hidden fields row. Used by _html_output."
    197211        if self._meta.html_class_for_hidden_fields_row:
     
    217231        if top_errors:
    218232            output.append(self.top_errors_html_output(top_errors, top_errors_row))
    219233        if self.has_fieldsets():
    220             for i, fieldset in enumerate(self._meta.fieldsets):
    221                 output_method = fieldset.get('html_output_method', self.__class__.fieldset_html_output)
    222                 fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
     234            inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines.
     235            for i, fieldset_or_inline in enumerate(self._meta.fieldsets):
    223236                is_first = (i == 0)
    224237                is_last = (i + 1 == len(self._meta.fieldsets))
    225                 output.append(output_method(self, fieldset, fields, fieldset_start, fieldset_end, is_first, is_last))
     238                if isinstance(fieldset_or_inline, dict):
     239                    output_method = fieldset_or_inline.get('html_output_method', self.__class__.fieldset_html_output)
     240                    fields = dict((name, visible_fields[name]) for name in fieldset_or_inline['fields'] if name in visible_fields)
     241                    output.append(output_method(self, fieldset_or_inline, fields, fieldset_start, fieldset_end, is_first, is_last))
     242                else:
     243                    output_method = fieldset_or_inline._meta.fieldset_html_output_method or self.__class__.inline_html_output
     244                    output.append(output_method(self, inlines.pop(0), fieldset_start, fieldset_end, is_first, is_last))
    226245        else:
    227246            for name in self.fields:
    228247                if name in visible_fields:
     
    243262        "Returns this form rendered as HTML <p>s."
    244263        return self._html_output('p', u'%s', u'%s', u'%s', u'<p%s>%s</p>')
    245264
     265    def as_tr(self):
     266        "Returns this form rendered as HTML <td>s."
     267        if self.has_fieldsets():
     268            raise ValueError("%s has fieldsets so its method as_tr cannot be used." % self.__class__)
     269        colspan = len([bf for bf in self if not bf.is_hidden])
     270        html_output = self._html_output('tr', u'<tr><td colspan="%s">%%s</td></tr>\n<tr>' % colspan, u'%s', u'%s', u'</tr>\n<tr%%s><td colspan="%s">%%s</td></tr>' % colspan)
     271        if not html_output.startswith('<tr>'):
     272            html_output = u'<tr>\n%s' % html_output
     273        if not html_output.endswith('<tr>'):
     274            html_output = u'%s\n</tr>' % html_output
     275        return html_output
     276
     277    def is_valid(self):
     278        """
     279        Returns True if the form and its inlines have no errors.
     280        """
     281        if self._is_valid is None:
     282            self.full_clean()
     283        return self._is_valid
     284
     285    def _get_errors(self):
     286        "Returns an ErrorDict for the data provided for the form."
     287        if self._is_valid is None:
     288            self.full_clean()
     289        return self._errors
     290    errors = property(_get_errors)
     291
    246292    def non_field_errors(self):
    247293        """
    248294        Returns an ErrorList of errors that aren't associated with a particular
     
    253299
    254300    def full_clean(self):
    255301        """
    256         Cleans all of self.data and populates self._errors and
     302        Cleans all of self.data and populates self._is_valid, self._errors and
    257303        self.cleaned_data.
    258304        """
     305        self._is_valid = True # Assume the form is valid until proven otherwise.
    259306        self._errors = ErrorDict()
    260307        if not self.is_bound: # Stop further processing.
    261308            return
    262309        self.cleaned_data = {}
     310        # Process fields.
    263311        if self._meta.validation_order:
    264312            items = [(name, self.fields[name]) for name in self._meta.validation_order]
    265313        else:
    266314            items = self.fields.items()
    267         for name, field in items:
    268             # value_from_datadict() gets the data from the data dictionaries.
    269             # Each widget type knows how to retrieve its own data, because some
    270             # widgets split data over several HTML fields.
    271             value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
     315        for bf in self:
    272316            try:
    273                 if isinstance(field, FileField):
    274                     initial = self.initial.get(name, field.initial)
    275                     value = field.clean(value, initial)
     317                if isinstance(bf.field, FileField):
     318                    initial = self.initial.get(name, bf.field.initial)
     319                    value = bf.field.clean(bf.data, initial)
    276320                else:
    277                     value = field.clean(value)
    278                 self.cleaned_data[name] = value
    279                 if hasattr(self, 'clean_%s' % name):
    280                     value = getattr(self, 'clean_%s' % name)()
    281                     self.cleaned_data[name] = value
     321                    value = bf.field.clean(bf.data)
     322                self.cleaned_data[bf.name] = value
     323                if hasattr(self, 'clean_%s' % bf.name):
     324                    self.cleaned_data[bf.name] = getattr(self, 'clean_%s' % bf.name)()
    282325            except ValidationError, e:
    283                 self._errors[name] = self._meta.error_class(e.messages)
    284                 if name in self.cleaned_data:
    285                     del self.cleaned_data[name]
     326                self._errors[bf.name] = self._meta.error_class(e.messages)
     327                self._is_valid = False
     328                if bf.name in self.cleaned_data:
     329                    del self.cleaned_data[bf.name]
     330        # Process inlines.
     331        for inline in self.inlines:
     332            inline.full_clean()
     333            if not inline.is_valid():
     334                self._is_valid = False
     335        # Give self.clean() a chance to do validation.
    286336        try:
    287337            self.cleaned_data = self.clean()
    288338        except ValidationError, e:
    289339            self._errors[NON_FIELD_ERRORS] = self._meta.error_class(e.messages)
    290         if self._errors:
     340            self._is_valid = False
     341        # If there were errors, remove the cleaned_data attribute.
     342        if not self._is_valid:
    291343            delattr(self, 'cleaned_data')
    292344
    293345    def clean(self):
     
    299351        """
    300352        return self.cleaned_data
    301353
     354    def is_empty(self, fields=None, exclude=None):
     355        """
     356        Returns True if the form (including inlines) is empty. Otherwise, False.
     357        """
     358        for bf in self:
     359            if fields and bf.name not in fields:
     360                continue
     361            if exclude and bf.name in exclude:
     362                continue
     363            if not bf.widget.is_empty(bf.data):
     364                return False
     365        for inline in self.inlines:
     366            if not inline.is_empty():
     367                return False
     368        return True
     369
     370    def reset(self):
     371        """
     372        Resets the form (including inlines) to the state it was in
     373        before data was passed to it.
     374        """
     375        self.is_bound = False
     376        self.data = {}
     377        self.files = {}
     378        self._is_valid = None
     379        for inline in self.inlines:
     380            inline.reset()
     381
    302382    def is_multipart(self):
    303383        """
    304         Returns True if the form needs to be multipart-encrypted, i.e. it has
    305         FileInput. Otherwise, False.
     384        Returns True if the form (including inlines) needs to be
     385        multipart-encrypted, i.e. it has FileInput. Otherwise, False.
    306386        """
    307387        for field in self.fields.values():
    308388            if field.widget.needs_multipart_form:
    309389                return True
     390        for inline in self.inlines:
     391            if inline.is_multipart():
     392                return True
    310393        return False
    311394
    312395class Form(BaseForm):
  • django/newforms/models.py

    === modified file 'django/newforms/models.py'
     
    55
    66from warnings import warn
    77
     8from django.core.exceptions import ImproperlyConfigured
    89from django.utils.translation import ugettext_lazy as _
    910from django.utils.encoding import smart_unicode
    1011from django.utils.datastructures import SortedDict, InheritableOptions
    1112
    1213from util import ValidationError
    1314from forms import FormOptions, FormMetaclass, BaseForm
    14 from fields import Field, ChoiceField, EMPTY_VALUES
     15from formsets import FormSetOptions, FormSetMetaclass, BaseFormSet, DELETION_FIELD_NAME
     16from fields import Field, ChoiceField, EMPTY_VALUES, IntegerField, HiddenInput
    1517from widgets import Select, SelectMultiple, MultipleHiddenInput
    1618
    1719__all__ = (
    1820    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    1921    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
    20     'ModelChoiceField', 'ModelMultipleChoiceField'
     22    'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelFormSet',
     23    'ModelFormSet', 'BaseInlineFormSet', 'InlineFormSet',
    2124)
    2225
    2326def save_instance(form, instance, fields=None, fail_message='saved',
     
    249252        cls.create_declared_fields(new_cls)
    250253        cls.create_base_fields_pool_from_model_fields_and_declared_fields(new_cls)
    251254        cls.create_base_fields_from_base_fields_pool(new_cls)
     255        cls.create_fieldsets_if_inlines_exist(new_cls)
    252256        return new_cls
    253257
    254258class BaseModelForm(BaseForm):
    255259    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, instance=None):
    256         opts = self._meta
    257260        if instance is None:
    258             # if we didn't get an instance, instantiate a new one
    259             self.instance = opts.model()
     261            # If we didn't get an instance, instantiate a new one.
     262            self.instance = self._meta.model()
    260263            object_data = {}
    261264        else:
    262265            self.instance = instance
    263266            object_data = model_to_dict(instance, self.base_fields.keys())
    264         # if initial was provided, it should override the values from instance
     267        # If initial was provided, it should override the values from instance.
    265268        if initial is not None:
    266269            object_data.update(initial)
    267         BaseForm.__init__(self, data, files, auto_id, prefix, object_data)
     270        # Create inlines.
     271        inlines = []
     272        if self.has_fieldsets():
     273            for fieldset_or_inline in self._meta.fieldsets:
     274                if not isinstance(fieldset_or_inline, dict):
     275                    inline_prefix = '%s-%s' % (prefix or 'inline', len(inlines) + 1)
     276                    inlines.append(fieldset_or_inline(data, files, auto_id, inline_prefix, instance))
     277        BaseForm.__init__(self, data, files, auto_id, prefix, object_data, inlines)
    268278
    269279    def save(self, commit=True):
    270280        """
     
    278288            fail_message = 'created'
    279289        else:
    280290            fail_message = 'changed'
    281         return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
     291        instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
     292        for inline in self.inlines:
     293            inline.save(commit)
     294        return instance
    282295
    283296class ModelForm(BaseModelForm):
    284297    __metaclass__ = ModelFormMetaclass
    285298    options = ModelFormOptions
    286299
    287 
    288300# Fields #####################################################################
    289301
    290302class QuerySetIterator(object):
     
    395407            else:
    396408                final_values.append(obj)
    397409        return final_values
     410
     411# Model-FormSet integration ###################################################
     412
     413class ModelFormSetOptions(InheritableOptions):
     414    _default_options = ModelFormOptions._default_options.copy()
     415    _default_options.update(FormSetOptions._formset_options)
     416    _default_options.update({
     417        'base_form': ModelForm,
     418    })
     419
     420class BaseModelFormSet(BaseFormSet):
     421    """
     422    A ``FormSet`` for editing a queryset and/or adding new objects to it.
     423    """
     424
     425    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None, **kwargs):
     426        if queryset is None:
     427            self.queryset = self.get_queryset(**kwargs)
     428        else:
     429            self.queryset = queryset
     430        initial = [model_to_dict(obj, self._meta.form._meta.base_fields.keys()) for obj in self.queryset]
     431        super(BaseModelFormSet, self).__init__(data, files, auto_id, prefix, initial)
     432
     433    def get_queryset(self, **kwargs):
     434        """
     435        Hook to returning a queryset for this model.
     436        """
     437        return self._meta.form._meta.model._default_manager.all()
     438
     439    def add_fields(self, form, index):
     440        """Add a hidden field for the object's primary key."""
     441        self._pk_field_name = self._meta.form._meta.model._meta.pk.attname
     442        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
     443        super(BaseModelFormSet, self).add_fields(form, index)
     444
     445    def save_new(self, form, commit=True):
     446        """Saves and returns a new model instance for the given form."""
     447        return save_instance(form, self._meta.form._meta.model(), commit=commit)
     448
     449    def save_instance(self, form, instance, commit=True):
     450        """Saves and returns an existing model instance for the given form."""
     451        return save_instance(form, instance, commit=commit)
     452
     453    def save(self, commit=True):
     454        """Saves model instances for every form, adding and changing instances
     455        as necessary, and returns the list of instances.
     456        """
     457        return self.save_existing_objects(commit) + self.save_new_objects(commit)
     458
     459    def save_existing_objects(self, commit=True):
     460        if not self.queryset:
     461            return []
     462        # Put the objects from self.queryset into a dict so they are easy to lookup by pk.
     463        existing_objects = {}
     464        for obj in self.queryset:
     465            existing_objects[obj.pk] = obj
     466        saved_instances = []
     467        for form in self.change_forms:
     468            obj = existing_objects[form.cleaned_data[self._pk_field_name]]
     469            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     470                obj.delete()
     471            else:
     472                saved_instances.append(self.save_instance(form, obj, commit=commit))
     473        return saved_instances
     474
     475    def save_new_objects(self, commit=True):
     476        new_objects = []
     477        for form in self.add_forms:
     478            if form.is_empty():
     479                continue
     480            # If someone has marked an add form for deletion, don't save the
     481            # object. At some point it would be nice if we didn't display
     482            # the deletion widget for add forms.
     483            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     484                continue
     485            new_objects.append(self.save_new(form, commit=commit))
     486        return new_objects
     487
     488class ModelFormSet(BaseModelFormSet):
     489    __metaclass__ = FormSetMetaclass
     490    options =  ModelFormSetOptions
     491
     492class InlineFormSetOptions(InheritableOptions):
     493    _model_formset_options = {
     494        'parent_model': None,
     495        'fk_name': None,
     496    }
     497    _default_options = ModelFormSetOptions._default_options.copy()
     498    _default_options.update(_model_formset_options)
     499    _default_options.update({
     500        'deletable': True,
     501        'num_extra': 3,
     502    })
     503
     504class InlineFormSetMetaclass(FormSetMetaclass):
     505
     506    def add_fk_attribute_and_remove_fk_from_base_fields(cls, new_cls):
     507        # Get options - if models are not set, this class wouldn't be used directly.
     508        parent_model, model, fk_name = new_cls._meta.parent_model, new_cls._meta.model, new_cls._meta.fk_name
     509        if not (parent_model and model):
     510            return
     511        # Try to discover what the foreign key from model to parent_model is.
     512        fks_to_parent = []
     513        for field in model._meta.fields:
     514            # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy.
     515            try:
     516                if field.rel.to == parent_model:
     517                    fks_to_parent.append(field)
     518            except AttributeError:
     519                pass
     520        if len(fks_to_parent) == 0:
     521            raise ImproperlyConfigured("%s has no ForeignKey to %s." % (model, parent_model))
     522        if fk_name:
     523            fks_to_parent = [fk for fk in fks_to_parent if fk.name == fk_name]
     524        if len(fks_to_parent) > 1:
     525            raise ImproperlyConfigured("%s has more than one ForeignKey to %s." % (model, parent_model))
     526        new_cls.fk = fks_to_parent[0]
     527        # Try to remove the foreign key from base_fields to keep it transparent to the form.
     528        try:
     529            del new_cls._meta.form.base_fields[new_cls.fk.name]
     530        except KeyError:
     531            pass
     532    add_fk_attribute_and_remove_fk_from_base_fields = classmethod(add_fk_attribute_and_remove_fk_from_base_fields)
     533
     534    def __new__(cls, name, bases, attrs):
     535        new_cls = type.__new__(cls, name, bases, attrs)
     536        cls.create_options(new_cls)
     537        cls.create_form_if_not_exists(new_cls)
     538        cls.check_no_fieldsets_in_form(new_cls)
     539        cls.add_fk_attribute_and_remove_fk_from_base_fields(new_cls)
     540        return new_cls
     541
     542class BaseInlineFormSet(BaseModelFormSet):
     543    """A formset for child objects related to a parent."""
     544
     545    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instance=None, **kwargs):
     546        self.instance = instance
     547        super(BaseInlineFormSet, self).__init__(data, files, auto_id, prefix, **kwargs)
     548
     549    def get_queryset(self, **kwargs):
     550        """
     551        Returns this FormSet's queryset, but restricted to children of
     552        self.instance
     553        """
     554        if self.instance is None:
     555            return self._meta.form._meta.model._default_manager.none()
     556        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     557        return queryset.filter(**{self.fk.name: self.instance})
     558
     559    def save_new(self, form, commit=True):
     560        kwargs = {self.fk.get_attname(): self.instance.pk}
     561        new_obj = self._meta.form._meta.model(**kwargs)
     562        return save_instance(form, new_obj, commit=commit)
     563
     564class InlineFormSet(BaseInlineFormSet):
     565    __metaclass__ = InlineFormSetMetaclass
     566    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>,
     
    104112            help_text = u' %s' % help_text
    105113        return u'%(rendered_errors)s<p%(row_attrs)s>%(label_tag)s %(rendered_widget)s%(help_text)s</p>' % locals()
    106114
     115    def for_tr(self, rendered_widget, rendered_errors, label_tag, help_text, row_attrs):
     116        "Returns this widget rendered as HTML <td>."
     117        if help_text:
     118            help_text = u' %s' % help_text
     119        return u'<td%(row_attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>' % locals()
     120
    107121class Input(Widget):
    108122    """
    109123    Base class for all <input> widgets (except type='checkbox' and
     
    228242            return False
    229243        return super(CheckboxInput, self).value_from_datadict(data, files, name)
    230244
     245    def is_empty(self, value):
     246        # This widget will always either be True or False, so always return the
     247        # opposite value so False values will make the form empty.
     248        return not value
     249
    231250class Select(Widget):
    232251    def __init__(self, attrs=None, row_attrs=None, choices=()):
    233252        super(Select, self).__init__(attrs, row_attrs)
     
    270289        value = data.get(name, None)
    271290        return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
    272291
     292    def is_empty(self, value):
     293        # This widget will always either be True or False, so always return the
     294        # opposite value so False values will make the form empty.
     295        return not value
     296
    273297class SelectMultiple(Select):
    274298    def render(self, name, value, attrs=None, choices=()):
    275299        if value is None: value = []
     
    461485    def value_from_datadict(self, data, files, name):
    462486        return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
    463487
     488    def is_empty(self, value):
     489        for widget, val in zip(self.widgets, value):
     490            if not widget.is_empty(val):
     491                return False
     492        return True
     493
    464494    def format_output(self, rendered_widgets):
    465495        """
    466496        Given a list of rendered widgets (as strings), returns a Unicode string
Back to Top