=== added file 'django/newforms/formsets.py'
--- django/newforms/formsets.py	1970-01-01 00:00:00 +0000
+++ django/newforms/formsets.py	2008-03-05 22:39:25 +0000
@@ -0,0 +1,294 @@
+from django.utils.encoding import StrAndUnicode
+from django.utils.translation import ugettext as _
+
+from fields import BooleanField, IntegerField
+from forms import Form, FormOptions
+from widgets import HiddenInput
+from util import ValidationError
+import metaclassing
+
+__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(FormOptions):
+    def __init__(self, options=None):
+        super(FormSetOptions, self).__init__(options)
+        # form
+        self.form = getattr(options, 'form', None)
+        self.base_form = getattr(options, 'base_form', Form)
+        # other options
+        self.deletable = getattr(options, 'deletable', False)
+        self.is_empty_fields = getattr(options, 'is_empty_fields', None)
+        self.is_empty_exclude = getattr(options, 'is_empty_exclude', None)
+        self.fieldset_attrs = getattr(options, 'fieldset_attrs', None)
+        self.fieldset_legend = getattr(options, 'fieldset_legend', None)
+        self.num_extra = getattr(options, 'num_extra', 1)
+        self.orderable = getattr(options, 'orderable', False)
+        self.output_type = getattr(options, 'output_type', 'tr')
+
+class FormSetMetaclass(type):
+    def __new__(cls, name, bases, attrs):
+        new_class = type.__new__(cls, name, bases, attrs)
+        metaclassing.create_meta(new_class)
+        metaclassing.create_form_if_not_exists(new_class)
+        metaclassing.check_no_fieldsets_in_form(new_class)
+        return new_class
+
+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):
+        # TODO START - replace this ugly hack
+        for field in self._meta.form.base_fields.values():
+            if hasattr(field, 'queryset'):
+                if field.cache_choices is False:
+                    field.cache_choices = True
+                    field.queryset._result_cache = None
+                    field.queryset = field.queryset
+                    field.cache_choices =  False
+        # TODO END - replace this ugly hack
+        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.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 = self._meta.is_empty_fields and list(self._meta.is_empty_fields) or []
+        exclude = self._meta.is_empty_exclude and list(self._meta.is_empty_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-03-05 22:50:21 +0000
+++ django/newforms/forms.py	2008-03-06 00:51:13 +0000
@@ -29,12 +29,14 @@
         self.fieldsets = getattr(options, 'fieldsets', None)
         self.fields = getattr(options, 'fields', None)
         self.exclude = getattr(options, 'exclude', None)
+        self.inlines = getattr(options, 'inlines', None)
         # other options
         self.error_class = getattr(options, 'error_class', ErrorList)
         self.error_row_class = getattr(options, 'error_row_class', 'error')
         self.hidden_row_class = getattr(options, 'hidden_row_class', 'hidden')
         self.label_capfirst = getattr(options, 'label_capfirst', True)
         self.label_suffix = getattr(options, 'label_suffix', ':')
+        self.output_type = getattr(options, 'output_type', 'table')
         self.required_row_class = getattr(options, 'required_row_class', 'required')
         self.validation_order = getattr(options, 'validation_order', None)
 
@@ -45,6 +47,7 @@
         metaclassing.create_declared_fields(new_class)
         metaclassing.create_base_fields_pool_from_declared_fields(new_class)
         metaclassing.create_base_fields_from_base_fields_pool(new_class)
+        metaclassing.create_fieldsets_if_inlines_exist(new_class)
         return new_class
 
 class BaseForm(StrAndUnicode):
@@ -52,14 +55,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
@@ -69,7 +73,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():
@@ -83,20 +87,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)
@@ -183,6 +173,26 @@
             output.append(fieldset_end_html)
         return u'\n'.join(output)
 
+    def _inline_html_output(self, fields, inline, is_first, is_last, fieldset_start_html, fieldset_end_html, legend_tag_html):
+        "Helper function for outputting HTML from a inline. Used by _html_output."
+        output = []
+        if not is_first:
+            legend_tag = attrs = u''
+            if inline._meta.fieldset_legend:
+                legend_tag = legend_tag_html % {
+                    'legend': conditional_escape(force_unicode(inline._meta.fieldset_legend)),
+                }
+            if inline._meta.fieldset_attrs:
+                attrs = flatatt(inline._meta.fieldset_attrs)
+            output.append(fieldset_start_html % {
+                'legend_tag': legend_tag,
+                'attrs': attrs,
+            })
+        output.append(unicode(inline))
+        if not is_last:
+            output.append(fieldset_end_html)
+        return u'\n'.join(output)
+
     def _hidden_fields_html_output(self, hidden_fields, hidden_fields_html):
         "Helper function for outputting HTML from a hidden fields. Used by _html_output."
         if self._meta.hidden_row_class:
@@ -210,12 +220,17 @@
         if top_errors:
             output.append(self._top_errors_html_output(top_errors, top_errors_html))
         if self.has_fieldsets():
-            for i, fieldset in enumerate(self._meta.fieldsets):
-                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(self._fieldset_html_output(fields, fieldset, is_first, is_last,
-                    fieldset_start_html, fieldset_end_html, legend_tag_html))
+                if isinstance(fieldset_or_inline, dict):
+                    fields = dict((name, visible_fields[name]) for name in fieldset_or_inline['fields'] if name in visible_fields)
+                    output.append(self._fieldset_html_output(fields, fieldset_or_inline, is_first, is_last,
+                        fieldset_start_html, fieldset_end_html, legend_tag_html))
+                else:
+                    output.append(self._inline_html_output(fields, inlines.pop(0), is_first, is_last,
+                        fieldset_start_html, fieldset_end_html, legend_tag_html))
         else:
             for name in self.fields:
                 if name in visible_fields:
@@ -266,6 +281,43 @@
         }
         return self._html_output(**kwargs)
 
+    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__.__name__)
+        colspan = len([bf for bf in self if not bf.is_hidden])
+        kwargs = {
+            'row_html': u'<td%(attrs)s>%(rendered_errors)s%(rendered_widget)s%(help_text)s</td>',
+            'label_tag_html': u'',
+            'help_text_html': u' %(help_text)s',
+            'top_errors_html': u'<tr><td colspan="%s">%%(top_errors)s</td></tr>\n<tr>' % colspan,
+            'fieldset_start_html': u'',
+            'fieldset_end_html': u'',
+            'legend_tag_html': u'',
+            'hidden_fields_html': u'</tr>\n<tr%%(attrs)s><td colspan="%s">%%(hidden_fields)s</td></tr>' % colspan,
+        }
+        html_output = self._html_output(**kwargs)
+        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
@@ -276,41 +328,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(bf.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):
@@ -322,14 +380,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/metaclassing.py'
--- django/newforms/metaclassing.py	2008-03-05 22:12:24 +0000
+++ django/newforms/metaclassing.py	2008-03-05 23:10:56 +0000
@@ -50,8 +50,9 @@
         raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__)
     if cls._meta.fieldsets:
         names = []
-        for fieldset in cls._meta.fieldsets:
-            names.extend(fieldset['fields'])
+        for fieldset_or_inline in cls._meta.fieldsets:
+            if isinstance(fieldset_or_inline, dict):
+                names.extend(fieldset_or_inline['fields'])
     elif cls._meta.fields:
         names = cls._meta.fields
     elif cls._meta.exclude:
@@ -59,3 +60,51 @@
     else:
         names = cls._base_fields_pool.keys()
     cls.base_fields = SortedDict([(name, cls._base_fields_pool[name]) for name in names])
+
+def create_fieldsets_if_inlines_exist(cls):
+    if cls._meta.inlines is not None:
+        if cls._meta.fieldsets is not None:
+            raise ImproperlyConfigured("%s cannot have more than one option from fieldsets and inlines." % cls.__name__)
+        cls._meta.fieldsets = [{'fields': cls.base_fields.keys()}] + list(cls._meta.inlines)
+
+def create_form_if_not_exists(cls):
+    if not cls._meta.form:
+        form_attrs = {
+            'Meta': type('Meta', (), cls._meta.__dict__),
+        }
+        for name, attr in cls.__dict__.items():
+            if isinstance(attr, Field):
+                form_attrs[name] = attr
+                delattr(cls, name)
+        cls._meta.form = type('%sForm' % cls.__name__, (cls._meta.base_form,), form_attrs)
+
+def check_no_fieldsets_in_form(cls):
+    if cls._meta.form._meta.fieldsets:
+        raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__)
+
+def add_fk_attribute_and_remove_fk_from_base_fields(cls):
+    # Get some options - if models are not set, this class would not be used directly.
+    parent_model, model, fk_name = cls._meta.parent_model, cls._meta.model, 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))
+    cls.fk = fks_to_parent[0]
+    # Try to remove the foreign key from base_fields to keep it transparent to the form.
+    try:
+        del cls._meta.form.base_fields[cls.fk.name]
+    except KeyError:
+        pass

=== modified file 'django/newforms/models.py'
--- django/newforms/models.py	2008-03-05 12:40:42 +0000
+++ django/newforms/models.py	2008-03-05 13:30:23 +0000
@@ -11,14 +11,16 @@
 
 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
 import metaclassing
 
 __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',
@@ -218,22 +220,29 @@
         metaclassing.create_declared_fields(new_class)
         metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class)
         metaclassing.create_base_fields_from_base_fields_pool(new_class)
+        metaclassing.create_fieldsets_if_inlines_exist(new_class)
         return new_class
 
 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))
+                    inlines.append(fieldset_or_inline(data, files, auto_id, inline_prefix, self.instance))
+        BaseForm.__init__(self, data, files, auto_id, prefix, object_data, inlines)
 
     def save(self, commit=True):
         """
@@ -247,13 +256,14 @@
             fail_message = 'created'
         else:
             fail_message = 'changed'
-        return save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
+        self.saved_instance = save_instance(self, self.instance, self.base_fields.keys(), fail_message, commit)
+        self.saved_inline_instances = [inline.save(commit) for inline in self.inlines]
+        return self.saved_instance
 
 class ModelForm(BaseModelForm):
     __metaclass__ = ModelFormMetaclass
     _options = ModelFormOptions
 
-
 # Fields #####################################################################
 
 class QuerySetIterator(object):
@@ -364,3 +374,133 @@
             else:
                 final_values.append(obj)
         return final_values
+
+# Model-FormSet integration ###################################################
+
+class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
+    def __init__(self, options=None):
+        super(ModelFormSetOptions, self).__init__(options)
+        # other default options
+        self.base_form = getattr(options, '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
+        self._pk_name = self._meta.model._meta.pk.name
+        self._pks = [obj.pk for obj in self.queryset]
+        initial = [model_to_dict(obj, self._meta.form.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."""
+        pk = index < len(self._pks) and self._pks[index] or None
+        form.fields[self._pk_name] = IntegerField(required=False, widget=HiddenInput, initial=pk)
+        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
+        # If commit is not permitted, objects cannot be deleted so they are saved as an instance variable.
+        if not commit:
+            self.instances_to_delete = []
+        saved_instances = []
+        for form in self.change_forms:
+            obj = existing_objects[form.cleaned_data[self._pk_name]]
+            if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
+                if commit:
+                    obj.delete()
+                else:
+                    self.instances_to_delete.append(obj)
+            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.
+            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):
+    _options =  ModelFormSetOptions
+
+class InlineFormSetOptions(ModelFormSetOptions):
+    def __init__(self, options=None):
+        super(InlineFormSetOptions, self).__init__(options)
+        # new options
+        self.parent_model = getattr(options, 'parent_model', None)
+        self.fk_name = getattr(options, 'fk_name', None)
+        # other default options
+        self.deletable = getattr(options, 'deletable', True)
+        self.num_extra = getattr(options, 'num_extra', 3)
+
+class InlineFormSetMetaclass(FormSetMetaclass):
+    def __new__(cls, name, bases, attrs):
+        new_class = type.__new__(cls, name, bases, attrs)
+        metaclassing.create_meta(new_class)
+        metaclassing.create_form_if_not_exists(new_class)
+        metaclassing.check_no_fieldsets_in_form(new_class)
+        metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class)
+        return new_class
+
+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.name: self.instance}
+        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-03-05 22:12:23 +0000
+++ django/newforms/widgets.py	2008-03-05 22:37:54 +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>,
@@ -206,6 +214,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)
@@ -248,6 +261,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 = []
@@ -439,6 +457,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

