=== added file 'django/newforms/formsets.py'
--- django/newforms/formsets.py	1970-01-01 00:00:00 +0000
+++ django/newforms/formsets.py	2008-02-20 13:33:59 +0000
@@ -0,0 +1,315 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.datastructures import InheritableOptions
+from django.utils.encoding import StrAndUnicode
+from django.utils.translation import ugettext as _
+
+from fields import Field, BooleanField, IntegerField
+from forms import Form, FormOptions
+from widgets import HiddenInput
+from util import ValidationError
+
+__all__ = ('BaseFormSet', 'FormSet')
+
+# Special field names.
+CHANGE_FORMS_COUNT_FIELD_NAME = 'CHANGE-FORMS-COUNT'
+ALL_FORMS_COUNT_FIELD_NAME = 'ALL-FORMS-COUNT'
+ORDERING_FIELD_NAME = 'ORDER'
+DELETION_FIELD_NAME = 'DELETE'
+
+class ManagementForm(Form):
+    """
+    ``ManagementForm`` is used to keep track of how many form instances
+    are displayed on the page. If adding new forms via javascript, you should
+    increment the count field of this form as well.
+    """
+    def __init__(self, *args, **kwargs):
+        self.base_fields[CHANGE_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
+        self.base_fields[ALL_FORMS_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
+        super(ManagementForm, self).__init__(*args, **kwargs)
+
+class FormSetOptions(InheritableOptions):
+    _formset_options = {
+        # form
+        'form': None,
+        'base_form': Form,
+        # other options
+        'deletable': False,
+        'emptiness_test_fields': None,
+        'emptiness_test_exclude': None,
+        'fieldset_attrs': {},
+        'fieldset_html_output_method': None,
+        'fieldset_legend': None,
+        'num_extra': 1,
+        'orderable': False,
+        'output_type': 'tr',
+    }
+    _default_options = FormOptions._default_options.copy()
+    _default_options.update(_formset_options)
+
+class FormSetMetaclass(type):
+
+    def create_options(cls, new_cls):
+        new_cls._meta = new_cls.options(new_cls)
+        try:
+            delattr(new_cls, 'Meta')
+        except AttributeError:
+            pass
+    create_options = classmethod(create_options)
+
+    def create_form_if_not_exists(cls, new_cls):
+        if not new_cls._meta.form:
+            form_attrs = {
+                'Meta': type('Meta', (), new_cls._meta.__dict__),
+            }
+            for name, attr in new_cls.__dict__.items():
+                if isinstance(attr, Field):
+                    form_attrs[name] = attr
+                    delattr(new_cls, name)
+            new_cls._meta.form = type('%sForm' % new_cls.__name__, (new_cls._meta.base_form,), form_attrs)
+    create_form_if_not_exists = classmethod(create_form_if_not_exists)
+
+    def check_no_fieldsets_in_form(cls, new_cls):
+        if new_cls._meta.form._meta.fieldsets is not None:
+            raise ImproperlyConfigured("%s cannot have fieldsets." % new_cls.__name__)
+    check_no_fieldsets_in_form = classmethod(check_no_fieldsets_in_form)
+
+    def __new__(cls, name, bases, attrs):
+        new_cls = type.__new__(cls, name, bases, attrs)
+        cls.create_options(new_cls)
+        cls.create_form_if_not_exists(new_cls)
+        cls.check_no_fieldsets_in_form(new_cls)
+        return new_cls
+
+class BaseFormSet(StrAndUnicode):
+    """A collection of instances of the same Form class."""
+
+    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
+        self.is_bound = data is not None or files is not None
+        self.data = data
+        self.files = files
+        self.auto_id = auto_id
+        self.prefix = prefix or 'form'
+        self.initial = initial
+        self._is_valid = None # Stores validation state after full_clean() has been called.
+        self._create_forms()
+
+    def _create_forms(self):
+        self._create_management_forms()
+        self._create_change_forms()
+        self._create_add_forms()
+        self.forms = self.change_forms + self.add_forms
+
+    def _create_management_forms(self):
+        # Initialization is different depending on whether we recieved data, initial, or nothing.
+        self.management_form = None
+        if self.data or self.files:
+            self.management_form = ManagementForm(self.data, self.files, auto_id=self.auto_id, prefix=self.prefix)
+            if self.management_form.is_valid():
+                self.change_forms_count = self.management_form.cleaned_data[CHANGE_FORMS_COUNT_FIELD_NAME]
+                self.all_forms_count = self.management_form.cleaned_data[ALL_FORMS_COUNT_FIELD_NAME]
+            else:
+                # ManagementForm data is missing or has been tampered with."
+                self.management_form = None
+        if not self.management_form:
+            self.change_forms_count = self.initial and len(self.initial) or 0
+            self.all_forms_count = self.change_forms_count + self._meta.num_extra
+            management_form_initial = {
+                CHANGE_FORMS_COUNT_FIELD_NAME: self.change_forms_count,
+                ALL_FORMS_COUNT_FIELD_NAME: self.all_forms_count,
+            }
+            self.management_form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial=management_form_initial)
+
+    def _create_change_forms(self):
+        self.change_forms = []
+        for i in range(0, self.change_forms_count):
+            kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+            if self.data:
+                kwargs['data'] = self.data
+            if self.files:
+                kwargs['files'] = self.files
+            if self.initial:
+                kwargs['initial'] = self.initial[i]
+            change_form = self._meta.form(**kwargs)
+            self.add_fields(change_form, i)
+            self.change_forms.append(change_form)
+
+    def _create_add_forms(self):
+        self.add_forms = []
+        for i in range(self.change_forms_count, self.all_forms_count):
+            kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+            if self.data:
+                kwargs['data'] = self.data
+            if self.files:
+                kwargs['files'] = self.files
+            add_form = self._meta.form(**kwargs)
+            self.add_fields(add_form, i)
+            self.add_forms.append(add_form)
+
+    def __unicode__(self):
+        return getattr(self, 'as_%s' % self._meta.output_type)()
+
+    def add_prefix(self, index):
+        return '%s-%s' % (self.prefix, index)
+
+    def add_fields(self, form, index):
+        """A hook for adding extra fields on to each form instance."""
+        if self._meta.orderable:
+            form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_('Order'), initial=index+1)
+        if self._meta.deletable:
+            form.fields[DELETION_FIELD_NAME] = BooleanField(label=_('Delete'), required=False)
+
+    def as_table(self):
+        "Returns this form rendered as HTML <tr>s."
+        return u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms)
+
+    def as_ul(self):
+        "Returns this form rendered as HTML <li>s."
+        return u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms)
+
+    def as_p(self):
+        "Returns this form rendered as HTML <p>s."
+        return u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms)
+
+    def as_tr(self):
+        "Returns this form rendered as HTML <td>s."
+        output = [self.management_form.as_tr()]
+        if self.forms:
+            output.append(u'<tr>')
+            output.extend(u'<th>%s</th>' % bf.label for bf in self.forms[0] if not bf.is_hidden)
+            output.append(u'</tr>')
+        output.extend(form.as_tr() for form in [self.management_form] + self.forms)
+        return '\n'.join(output)
+
+    def is_valid(self):
+        """
+        Returns True if the formset (and its forms) have no errors.
+        """
+        if self._is_valid is None:
+            self.full_clean()
+        return self._is_valid
+
+    def _get_errors(self):
+        """
+        Returns list of ErrorDict for all forms.
+        """
+        if self._is_valid is None:
+            self.full_clean()
+        return self._errors
+    errors = property(_get_errors)
+
+    def _get_non_form_errors(self):
+        """
+        Returns an ErrorList of errors that aren't associated with a particular
+        form -- i.e., from formset.clean(). Returns an empty ErrorList if there
+        are none.
+        """
+        if self._is_valid is None:
+            self.full_clean()
+        return self._non_form_errors
+    non_form_errors = property(_get_non_form_errors)
+
+    def full_clean(self):
+        """
+        Cleans all of self.data and populates self._is_valid, self._errors,
+        self._no_form_errors, self.cleaned_data and self.deleted_data.
+        """
+        self._is_valid = True # Assume the formset is valid until proven otherwise.
+        self._errors = []
+        self._non_form_errors = self._meta.error_class()
+        if not self.is_bound: # Stop further processing.
+            return
+        self.cleaned_data = []
+        self.deleted_data = []
+        # Process change forms.
+        for form in self.change_forms:
+            if form.is_valid():
+                if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+                    self.deleted_data.append(form.cleaned_data)
+                else:
+                    self.cleaned_data.append(form.cleaned_data)
+            else:
+                self._is_valid = False
+            self._errors.append(form.errors)
+        # Process add forms in reverse so we can easily tell when the remaining
+        # ones should be required.
+        remaining_forms_required = False
+        add_errors = []
+        for i in range(len(self.add_forms)-1, -1, -1):
+            form = self.add_forms[i]
+            # If an add form is empty, reset it so it won't have any errors.
+            if not remaining_forms_required and self.is_empty(form):
+                form.reset()
+                continue
+            else:
+                remaining_forms_required = True
+                if form.is_valid():
+                    self.cleaned_data.append(form.cleaned_data)
+                else:
+                    self._is_valid = False
+            add_errors.append(form.errors)
+        add_errors.reverse()
+        self._errors.extend(add_errors)
+        # Sort cleaned_data if the formset is orderable.
+        if self._meta.orderable:
+            self.cleaned_data.sort(lambda x, y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
+        # Give self.clean() a chance to do validation.
+        try:
+            self.cleaned_data = self.clean()
+        except ValidationError, e:
+            self._non_form_errors = self._meta.error_class(e.messages)
+            self._is_valid = False
+        # If there were errors, remove the cleaned_data and deleted_data attributes.
+        if not self._is_valid:
+            delattr(self, 'cleaned_data')
+            delattr(self, 'deleted_data')
+
+    def clean(self):
+        """
+        Hook for doing any extra formset-wide cleaning after Form.clean() has
+        been called on every form. Any ValidationError raised by this method
+        will not be associated with a particular form; it will be accesible
+        via formset.non_form_errors()
+        """
+        return self.cleaned_data
+
+    def is_empty(self, form=None):
+        """
+        Returns True if the formset (including forms) or selected form
+        is empty. Otherwise, False.
+        """
+        fields = list(self._meta.emptiness_test_fields) or []
+        exclude = list(self._meta.emptiness_test_exclude) or []
+        if self._meta.orderable:
+            exclude.append(ORDERING_FIELD_NAME)
+        if form is None:
+            for form in self:
+                if not form.is_empty(fields, exclude):
+                    return False
+            return True
+        else:
+            return form.is_empty(fields, exclude)
+
+    def reset(self):
+        """
+        Resets the formset (including forms) to the state it was in
+        before data was passed to it.
+        """
+        self.is_bound = False
+        self.data = {}
+        self.files = {}
+        self._is_valid = None
+        self._create_forms()
+
+    def is_multipart(self):
+        """
+        Returns True if the formset needs to be multipart-encrypted, i.e. its
+        form has FileInput. Otherwise, False.
+        """
+        if self.forms:
+            return self.forms[0].is_multipart()
+        else:
+            return False
+
+class FormSet(BaseFormSet):
+    __metaclass__ = FormSetMetaclass
+    options = FormSetOptions

=== modified file 'django/newforms/__init__.py'
--- django/newforms/__init__.py	2008-01-28 21:24:08 +0000
+++ django/newforms/__init__.py	2008-02-07 21:06:22 +0000
@@ -15,3 +15,4 @@
 from fields import *
 from forms import *
 from models import *
+from formsets import *

=== modified file 'django/newforms/forms.py'
--- django/newforms/forms.py	2008-02-09 00:13:55 +0000
+++ django/newforms/forms.py	2008-02-14 12:12:57 +0000
@@ -4,6 +4,7 @@
 
 from copy import deepcopy
 
+from django.core.exceptions import ImproperlyConfigured
 from django.utils.datastructures import SortedDict, InheritableOptions
 from django.utils.html import conditional_escape
 from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
@@ -29,6 +30,7 @@
         'fieldsets': None,
         'fields': None,
         'exclude': None,
+        'inlines': None,
         # other options
         'error_class': ErrorList,
         'formfield_for_formfield': lambda self, formfield: formfield,
@@ -37,6 +39,7 @@
         'html_class_for_required_fields': 'required',
         'label_capitalization': True,
         'label_suffix': ':',
+        'output_type': 'table',
         'validation_order': None,
     }
 
@@ -76,8 +79,9 @@
     def create_base_fields_from_base_fields_pool(cls, new_cls):
         if new_cls._meta.fieldsets:
             names = []
-            for fieldset in new_cls._meta.fieldsets:
-                names.extend(fieldset['fields'])
+            for fieldset_or_inline in new_cls._meta.fieldsets:
+                if isinstance(fieldset_or_inline, dict):
+                    names.extend(fieldset_or_inline['fields'])
         elif new_cls._meta.fields:
             names = new_cls._meta.fields
         elif new_cls._meta.exclude:
@@ -87,12 +91,20 @@
         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])
     create_base_fields_from_base_fields_pool = classmethod(create_base_fields_from_base_fields_pool)
 
+    def create_fieldsets_if_inlines_exist(cls, new_cls):
+        if new_cls._meta.inlines is not None:
+            if new_cls._meta.fieldsets is not None:
+                raise ImproperlyConfigured("Options fieldsets and inlines cannot be used together.")
+            new_cls._meta.fieldsets = [{'fields': new_cls.base_fields.keys()}] + list(new_cls._meta.inlines)
+    create_fieldsets_if_inlines_exist = classmethod(create_fieldsets_if_inlines_exist)
+
     def __new__(cls, name, bases, attrs):
         new_cls = type.__new__(cls, name, bases, attrs)
         cls.create_options(new_cls)
         cls.create_declared_fields(new_cls)
         cls.create_base_fields_pool_from_declared_fields(new_cls)
         cls.create_base_fields_from_base_fields_pool(new_cls)
+        cls.create_fieldsets_if_inlines_exist(new_cls)
         return new_cls
 
 class BaseForm(StrAndUnicode):
@@ -100,14 +112,15 @@
     # class is different than Form. See the comments by the Form class for more
     # information. Any improvements to the form API should be made to *this*
     # class, not to the Form class.
-    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
+    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, inlines=None):
         self.is_bound = data is not None or files is not None
         self.data = data or {}
         self.files = files or {}
         self.auto_id = auto_id
         self.prefix = prefix
         self.initial = initial or {}
-        self._errors = None # Stores the errors after clean() has been called.
+        self.inlines = inlines or [] # It is not responsibility of this class to create inlines.
+        self._is_valid = None # Stores validation state after full_clean() has been called.
 
         # The base_fields class attribute is the *class-wide* definition of
         # fields. Because a particular *instance* of the class might want to
@@ -117,7 +130,7 @@
         self.fields = deepcopy(self.base_fields)
 
     def __unicode__(self):
-        return self.as_table()
+        return getattr(self, 'as_%s' % self._meta.output_type)()
 
     def __iter__(self):
         for name, field in self.fields.items():
@@ -131,20 +144,6 @@
             raise KeyError('Key %r not found in Form' % name)
         return BoundField(self, field, name)
 
-    def _get_errors(self):
-        "Returns an ErrorDict for the data provided for the form."
-        if self._errors is None:
-            self.full_clean()
-        return self._errors
-    errors = property(_get_errors)
-
-    def is_valid(self):
-        """
-        Returns True if the form has no errors. Otherwise, False. If errors are
-        being ignored, returns False.
-        """
-        return self.is_bound and not bool(self.errors)
-
     def has_fieldsets(self):
         "Returns True if this form has fieldsets."
         return bool(self._meta.fieldsets)
@@ -192,6 +191,21 @@
             output.append(fieldset_end % u'</fieldset>')
         return u'\n'.join(output)
 
+    def inline_html_output(self, inline, fieldset_start, fieldset_end, is_first, is_last):
+        "Helper function for outputting HTML from a inline. Used by _html_output."
+        output = []
+        if fieldset_start and not is_first:
+            fieldset_attrs = flatatt(inline._meta.fieldset_attrs)
+            if inline._meta.fieldset_legend:
+                legend_tag = u'\n<legend>%s</legend>' % conditional_escape(force_unicode(inline._meta.fieldset_legend))
+            else:
+                legend_tag = u''
+            output.append(fieldset_start % (u'<fieldset%s>%s' % (fieldset_attrs, legend_tag)))
+        output.append(unicode(inline))
+        if fieldset_end and not is_last:
+            output.append(fieldset_end % u'</fieldset>')
+        return u'\n'.join(output)
+
     def hidden_fields_html_output(self, hidden_fields, hidden_fields_row):
         "Helper function for outputting HTML from a hidden fields row. Used by _html_output."
         if self._meta.html_class_for_hidden_fields_row:
@@ -217,12 +231,17 @@
         if top_errors:
             output.append(self.top_errors_html_output(top_errors, top_errors_row))
         if self.has_fieldsets():
-            for i, fieldset in enumerate(self._meta.fieldsets):
-                output_method = fieldset.get('html_output_method', self.__class__.fieldset_html_output)
-                fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
+            inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines. 
+            for i, fieldset_or_inline in enumerate(self._meta.fieldsets):
                 is_first = (i == 0)
                 is_last = (i + 1 == len(self._meta.fieldsets))
-                output.append(output_method(self, fieldset, fields, fieldset_start, fieldset_end, is_first, is_last))
+                if isinstance(fieldset_or_inline, dict):
+                    output_method = fieldset_or_inline.get('html_output_method', self.__class__.fieldset_html_output)
+                    fields = dict((name, visible_fields[name]) for name in fieldset_or_inline['fields'] if name in visible_fields)
+                    output.append(output_method(self, fieldset_or_inline, fields, fieldset_start, fieldset_end, is_first, is_last))
+                else:
+                    output_method = fieldset_or_inline._meta.fieldset_html_output_method or self.__class__.inline_html_output
+                    output.append(output_method(self, inlines.pop(0), fieldset_start, fieldset_end, is_first, is_last))
         else:
             for name in self.fields:
                 if name in visible_fields:
@@ -243,6 +262,33 @@
         "Returns this form rendered as HTML <p>s."
         return self._html_output('p', u'%s', u'%s', u'%s', u'<p%s>%s</p>')
 
+    def as_tr(self):
+        "Returns this form rendered as HTML <td>s."
+        if self.has_fieldsets():
+            raise ValueError("%s has fieldsets so its method as_tr cannot be used." % self.__class__)
+        colspan = len([bf for bf in self if not bf.is_hidden])
+        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)
+        if not html_output.startswith('<tr>'):
+            html_output = u'<tr>\n%s' % html_output
+        if not html_output.endswith('<tr>'):
+            html_output = u'%s\n</tr>' % html_output
+        return html_output
+
+    def is_valid(self):
+        """
+        Returns True if the form and its inlines have no errors.
+        """
+        if self._is_valid is None:
+            self.full_clean()
+        return self._is_valid
+
+    def _get_errors(self):
+        "Returns an ErrorDict for the data provided for the form."
+        if self._is_valid is None:
+            self.full_clean()
+        return self._errors
+    errors = property(_get_errors)
+
     def non_field_errors(self):
         """
         Returns an ErrorList of errors that aren't associated with a particular
@@ -253,41 +299,47 @@
 
     def full_clean(self):
         """
-        Cleans all of self.data and populates self._errors and
+        Cleans all of self.data and populates self._is_valid, self._errors and
         self.cleaned_data.
         """
+        self._is_valid = True # Assume the form is valid until proven otherwise.
         self._errors = ErrorDict()
         if not self.is_bound: # Stop further processing.
             return
         self.cleaned_data = {}
+        # Process fields.
         if self._meta.validation_order:
             items = [(name, self.fields[name]) for name in self._meta.validation_order]
         else:
             items = self.fields.items()
-        for name, field in items:
-            # value_from_datadict() gets the data from the data dictionaries.
-            # Each widget type knows how to retrieve its own data, because some
-            # widgets split data over several HTML fields.
-            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
+        for bf in self:
             try:
-                if isinstance(field, FileField):
-                    initial = self.initial.get(name, field.initial)
-                    value = field.clean(value, initial)
+                if isinstance(bf.field, FileField):
+                    initial = self.initial.get(name, bf.field.initial)
+                    value = bf.field.clean(bf.data, initial)
                 else:
-                    value = field.clean(value)
-                self.cleaned_data[name] = value
-                if hasattr(self, 'clean_%s' % name):
-                    value = getattr(self, 'clean_%s' % name)()
-                    self.cleaned_data[name] = value
+                    value = bf.field.clean(bf.data)
+                self.cleaned_data[bf.name] = value
+                if hasattr(self, 'clean_%s' % bf.name):
+                    self.cleaned_data[bf.name] = getattr(self, 'clean_%s' % bf.name)()
             except ValidationError, e:
-                self._errors[name] = self._meta.error_class(e.messages)
-                if name in self.cleaned_data:
-                    del self.cleaned_data[name]
+                self._errors[bf.name] = self._meta.error_class(e.messages)
+                self._is_valid = False
+                if bf.name in self.cleaned_data:
+                    del self.cleaned_data[bf.name]
+        # Process inlines.
+        for inline in self.inlines:
+            inline.full_clean()
+            if not inline.is_valid():
+                self._is_valid = False
+        # Give self.clean() a chance to do validation.
         try:
             self.cleaned_data = self.clean()
         except ValidationError, e:
             self._errors[NON_FIELD_ERRORS] = self._meta.error_class(e.messages)
-        if self._errors:
+            self._is_valid = False
+        # If there were errors, remove the cleaned_data attribute.
+        if not self._is_valid:
             delattr(self, 'cleaned_data')
 
     def clean(self):
@@ -299,14 +351,45 @@
         """
         return self.cleaned_data
 
+    def is_empty(self, fields=None, exclude=None):
+        """
+        Returns True if the form (including inlines) is empty. Otherwise, False.
+        """
+        for bf in self:
+            if fields and bf.name not in fields:
+                continue
+            if exclude and bf.name in exclude:
+                continue
+            if not bf.widget.is_empty(bf.data):
+                return False
+        for inline in self.inlines:
+            if not inline.is_empty():
+                return False
+        return True
+
+    def reset(self):
+        """
+        Resets the form (including inlines) to the state it was in
+        before data was passed to it.
+        """
+        self.is_bound = False
+        self.data = {}
+        self.files = {}
+        self._is_valid = None
+        for inline in self.inlines:
+            inline.reset()
+
     def is_multipart(self):
         """
-        Returns True if the form needs to be multipart-encrypted, i.e. it has
-        FileInput. Otherwise, False.
+        Returns True if the form (including inlines) needs to be
+        multipart-encrypted, i.e. it has FileInput. Otherwise, False.
         """
         for field in self.fields.values():
             if field.widget.needs_multipart_form:
                 return True
+        for inline in self.inlines:
+            if inline.is_multipart():
+                return True
         return False
 
 class Form(BaseForm):

=== modified file 'django/newforms/models.py'
--- django/newforms/models.py	2008-02-18 15:00:59 +0000
+++ django/newforms/models.py	2008-02-20 13:56:19 +0000
@@ -5,19 +5,22 @@
 
 from warnings import warn
 
+from django.core.exceptions import ImproperlyConfigured
 from django.utils.translation import ugettext_lazy as _
 from django.utils.encoding import smart_unicode
 from django.utils.datastructures import SortedDict, InheritableOptions
 
 from util import ValidationError
 from forms import FormOptions, FormMetaclass, BaseForm
-from fields import Field, ChoiceField, EMPTY_VALUES
+from formsets import FormSetOptions, FormSetMetaclass, BaseFormSet, DELETION_FIELD_NAME
+from fields import Field, ChoiceField, EMPTY_VALUES, IntegerField, HiddenInput
 from widgets import Select, SelectMultiple, MultipleHiddenInput
 
 __all__ = (
     'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
     'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
-    'ModelChoiceField', 'ModelMultipleChoiceField'
+    'ModelChoiceField', 'ModelMultipleChoiceField', 'BaseModelFormSet',
+    'ModelFormSet', 'BaseInlineFormSet', 'InlineFormSet',
 )
 
 def save_instance(form, instance, fields=None, fail_message='saved',
@@ -249,22 +252,29 @@
         cls.create_declared_fields(new_cls)
         cls.create_base_fields_pool_from_model_fields_and_declared_fields(new_cls)
         cls.create_base_fields_from_base_fields_pool(new_cls)
+        cls.create_fieldsets_if_inlines_exist(new_cls)
         return new_cls
 
 class BaseModelForm(BaseForm):
     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, instance=None):
-        opts = self._meta
         if instance is None:
-            # if we didn't get an instance, instantiate a new one
-            self.instance = opts.model()
+            # If we didn't get an instance, instantiate a new one.
+            self.instance = self._meta.model()
             object_data = {}
         else:
             self.instance = instance
             object_data = model_to_dict(instance, self.base_fields.keys())
-        # if initial was provided, it should override the values from instance
+        # If initial was provided, it should override the values from instance.
         if initial is not None:
             object_data.update(initial)
-        BaseForm.__init__(self, data, files, auto_id, prefix, object_data)
+        # Create inlines.
+        inlines = []
+        if self.has_fieldsets():
+            for fieldset_or_inline in self._meta.fieldsets:
+                if not isinstance(fieldset_or_inline, dict):
+                    inline_prefix = '%s-%s' % (prefix or 'inline', len(inlines) + 1)
+                    inlines.append(fieldset_or_inline(data, files, auto_id, inline_prefix, instance))
+        BaseForm.__init__(self, data, files, auto_id, prefix, object_data, inlines)
 
     def save(self, commit=True):
         """
@@ -278,13 +288,15 @@
             fail_message = 'created'
         else:
             fail_message = 'changed'
-        return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
+        instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
+        for inline in self.inlines:
+            inline.save(commit)
+        return instance
 
 class ModelForm(BaseModelForm):
     __metaclass__ = ModelFormMetaclass
     options = ModelFormOptions
 
-
 # Fields #####################################################################
 
 class QuerySetIterator(object):
@@ -395,3 +407,160 @@
             else:
                 final_values.append(obj)
         return final_values
+
+# Model-FormSet integration ###################################################
+
+class ModelFormSetOptions(InheritableOptions):
+    _default_options = ModelFormOptions._default_options.copy()
+    _default_options.update(FormSetOptions._formset_options)
+    _default_options.update({
+        'base_form': ModelForm,
+    })
+
+class BaseModelFormSet(BaseFormSet):
+    """
+    A ``FormSet`` for editing a queryset and/or adding new objects to it.
+    """
+
+    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None, **kwargs):
+        if queryset is None:
+            self.queryset = self.get_queryset(**kwargs)
+        else:
+            self.queryset = queryset
+        initial = [model_to_dict(obj, self._meta.form._meta.base_fields.keys()) for obj in self.queryset]
+        super(BaseModelFormSet, self).__init__(data, files, auto_id, prefix, initial)
+
+    def get_queryset(self, **kwargs):
+        """
+        Hook to returning a queryset for this model.
+        """
+        return self._meta.form._meta.model._default_manager.all()
+
+    def add_fields(self, form, index):
+        """Add a hidden field for the object's primary key."""
+        self._pk_field_name = self._meta.form._meta.model._meta.pk.attname
+        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
+        super(BaseModelFormSet, self).add_fields(form, index)
+
+    def save_new(self, form, commit=True):
+        """Saves and returns a new model instance for the given form."""
+        return save_instance(form, self._meta.form._meta.model(), commit=commit)
+
+    def save_instance(self, form, instance, commit=True):
+        """Saves and returns an existing model instance for the given form."""
+        return save_instance(form, instance, commit=commit)
+
+    def save(self, commit=True):
+        """Saves model instances for every form, adding and changing instances
+        as necessary, and returns the list of instances.
+        """
+        return self.save_existing_objects(commit) + self.save_new_objects(commit)
+
+    def save_existing_objects(self, commit=True):
+        if not self.queryset:
+            return []
+        # Put the objects from self.queryset into a dict so they are easy to lookup by pk.
+        existing_objects = {}
+        for obj in self.queryset:
+            existing_objects[obj.pk] = obj
+        saved_instances = []
+        for form in self.change_forms:
+            obj = existing_objects[form.cleaned_data[self._pk_field_name]]
+            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+                obj.delete()
+            else:
+                saved_instances.append(self.save_instance(form, obj, commit=commit))
+        return saved_instances
+
+    def save_new_objects(self, commit=True):
+        new_objects = []
+        for form in self.add_forms:
+            if form.is_empty():
+                continue
+            # If someone has marked an add form for deletion, don't save the
+            # object. At some point it would be nice if we didn't display
+            # the deletion widget for add forms.
+            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+                continue
+            new_objects.append(self.save_new(form, commit=commit))
+        return new_objects
+
+class ModelFormSet(BaseModelFormSet):
+    __metaclass__ = FormSetMetaclass
+    options =  ModelFormSetOptions
+
+class InlineFormSetOptions(InheritableOptions):
+    _model_formset_options = {
+        'parent_model': None,
+        'fk_name': None,
+    }
+    _default_options = ModelFormSetOptions._default_options.copy()
+    _default_options.update(_model_formset_options)
+    _default_options.update({
+        'deletable': True,
+        'num_extra': 3,
+    })
+
+class InlineFormSetMetaclass(FormSetMetaclass):
+
+    def add_fk_attribute_and_remove_fk_from_base_fields(cls, new_cls):
+        # Get options - if models are not set, this class wouldn't be used directly.
+        parent_model, model, fk_name = new_cls._meta.parent_model, new_cls._meta.model, new_cls._meta.fk_name
+        if not (parent_model and model):
+            return
+        # Try to discover what the foreign key from model to parent_model is.
+        fks_to_parent = []
+        for field in model._meta.fields:
+            # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy.
+            try:
+                if field.rel.to == parent_model:
+                    fks_to_parent.append(field)
+            except AttributeError:
+                pass
+        if len(fks_to_parent) == 0:
+            raise ImproperlyConfigured("%s has no ForeignKey to %s." % (model, parent_model))
+        if fk_name:
+            fks_to_parent = [fk for fk in fks_to_parent if fk.name == fk_name]
+        if len(fks_to_parent) > 1:
+            raise ImproperlyConfigured("%s has more than one ForeignKey to %s." % (model, parent_model))
+        new_cls.fk = fks_to_parent[0]
+        # Try to remove the foreign key from base_fields to keep it transparent to the form.
+        try:
+            del new_cls._meta.form.base_fields[new_cls.fk.name]
+        except KeyError:
+            pass
+    add_fk_attribute_and_remove_fk_from_base_fields = classmethod(add_fk_attribute_and_remove_fk_from_base_fields)
+
+    def __new__(cls, name, bases, attrs):
+        new_cls = type.__new__(cls, name, bases, attrs)
+        cls.create_options(new_cls)
+        cls.create_form_if_not_exists(new_cls)
+        cls.check_no_fieldsets_in_form(new_cls)
+        cls.add_fk_attribute_and_remove_fk_from_base_fields(new_cls)
+        return new_cls
+
+class BaseInlineFormSet(BaseModelFormSet):
+    """A formset for child objects related to a parent."""
+
+    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, instance=None, **kwargs):
+        self.instance = instance
+        super(BaseInlineFormSet, self).__init__(data, files, auto_id, prefix, **kwargs)
+
+    def get_queryset(self, **kwargs):
+        """
+        Returns this FormSet's queryset, but restricted to children of
+        self.instance
+        """
+        if self.instance is None:
+            return self._meta.form._meta.model._default_manager.none()
+        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
+        return queryset.filter(**{self.fk.name: self.instance})
+
+    def save_new(self, form, commit=True):
+        kwargs = {self.fk.get_attname(): self.instance.pk}
+        new_obj = self._meta.form._meta.model(**kwargs)
+        return save_instance(form, new_obj, commit=commit)
+
+class InlineFormSet(BaseInlineFormSet):
+    __metaclass__ = InlineFormSetMetaclass
+    options = InlineFormSetOptions

=== modified file 'django/newforms/widgets.py'
--- django/newforms/widgets.py	2008-02-07 20:59:35 +0000
+++ django/newforms/widgets.py	2008-02-07 21:22:09 +0000
@@ -69,6 +69,14 @@
         """
         return data.get(name, None)
 
+    def is_empty(self, value):
+        """
+        Returns True if this form is empty.
+        """
+        if value not in (None, ''):
+            return False
+        return True
+
     def id_for_label(self, id_):
         """
         Returns the HTML ID attribute of this Widget for use by a <label>,
@@ -104,6 +112,12 @@
             help_text = u' %s' % help_text
         return u'%(rendered_errors)s<p%(row_attrs)s>%(label_tag)s %(rendered_widget)s%(help_text)s</p>' % locals()
 
+    def for_tr(self, rendered_widget, rendered_errors, label_tag, help_text, row_attrs):
+        "Returns this widget rendered as HTML <td>."
+        if help_text:
+            help_text = u' %s' % help_text
+        return u'<td%(row_attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>' % locals()
+
 class Input(Widget):
     """
     Base class for all <input> widgets (except type='checkbox' and
@@ -228,6 +242,11 @@
             return False
         return super(CheckboxInput, self).value_from_datadict(data, files, name)
 
+    def is_empty(self, value):
+        # This widget will always either be True or False, so always return the
+        # opposite value so False values will make the form empty.
+        return not value
+
 class Select(Widget):
     def __init__(self, attrs=None, row_attrs=None, choices=()):
         super(Select, self).__init__(attrs, row_attrs)
@@ -270,6 +289,11 @@
         value = data.get(name, None)
         return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
 
+    def is_empty(self, value):
+        # This widget will always either be True or False, so always return the
+        # opposite value so False values will make the form empty.
+        return not value
+
 class SelectMultiple(Select):
     def render(self, name, value, attrs=None, choices=()):
         if value is None: value = []
@@ -461,6 +485,12 @@
     def value_from_datadict(self, data, files, name):
         return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
 
+    def is_empty(self, value):
+        for widget, val in zip(self.widgets, value):
+            if not widget.is_empty(val):
+                return False
+        return True
+
     def format_output(self, rendered_widgets):
         """
         Given a list of rendered widgets (as strings), returns a Unicode string

