=== modified file 'django/contrib/admin/options.py'
--- django/contrib/admin/options.py	2008-08-02 08:47:58 +0000
+++ django/contrib/admin/options.py	2008-08-02 08:48:05 +0000
@@ -1,7 +1,7 @@
 from django import forms, template
 from django.forms.formsets import all_valid
 from django.forms.models import modelform_factory, inlineformset_factory
-from django.forms.models import BaseInlineFormset
+from django.forms.models import BaseInlineFormSet
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.admin import widgets
 from django.contrib.admin.util import quote, unquote, get_deleted_objects
@@ -714,7 +714,7 @@
     """
     model = None
     fk_name = None
-    formset = BaseInlineFormset
+    formset = BaseInlineFormSet
     extra = 3
     max_num = 0
     template = None

=== modified file 'django/forms/__init__.py'
--- django/forms/__init__.py	2008-07-20 21:58:25 +0000
+++ django/forms/__init__.py	2008-07-31 10:28:22 +0000
@@ -14,4 +14,5 @@
 from widgets import *
 from fields import *
 from forms import *
+from formsets import *
 from models import *

=== modified file 'django/forms/forms.py'
--- django/forms/forms.py	2008-08-01 09:07:13 +0000
+++ django/forms/forms.py	2008-08-01 09:12:00 +0000
@@ -30,6 +30,7 @@
         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')
@@ -38,6 +39,7 @@
         self.label_capfirst = getattr(options, 'label_capfirst', True)
         # self.label_capfirst = getattr(options, 'label_capfirst', False) # backward-compatible
         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.required_row_class = getattr(options, 'required_row_class', None) # backward-compatible
         self.use_field_row_class = getattr(options, 'use_field_row_class', True)
@@ -50,6 +52,7 @@
         metaclassing.create_declared_fields(new_class, attrs)
         metaclassing.create_base_fields_pool_from_declared_fields(new_class, attrs)
         metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
+        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
         metaclassing.create_media(new_class, attrs)
         return new_class
 
@@ -75,18 +78,26 @@
         if label_suffix is not None:
             self.label_suffix = label_suffix
         self.empty_permitted = empty_permitted
-        self._errors = None # Stores the errors after clean() has been called.
+        self._is_valid = None # Stores validation state after full_clean() has been called.
         self._changed_data = None
-
         # The base_fields class attribute is the *class-wide* definition of
         # fields. Because a particular *instance* of the class might want to
         # alter self.fields, we create self.fields here by copying base_fields.
         # Instances should always modify self.fields; they should not modify
         # self.base_fields.
         self.fields = deepcopy(self.base_fields)
+        self._construct_inlines()
+
+    def _construct_inlines(self):
+        # this class cannot create any inlines
+        self.inlines = []
+        if self.has_fieldsets():
+            for fieldset in self._meta.fieldsets:
+                if not isinstance(fieldset, dict):
+                    raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__)) 
 
     def __unicode__(self):
-        return self.as_table()
+        return getattr(self, 'as_%s' % self.output_type)()
 
     def __iter__(self):
         for name, field in self.fields.items():
@@ -102,7 +113,7 @@
 
     def _get_errors(self):
         "Returns an ErrorDict for the data provided for the form."
-        if self._errors is None:
+        if self._is_valid is None:
             self.full_clean()
         return self._errors
     errors = property(_get_errors)
@@ -112,7 +123,9 @@
         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)
+        if self._is_valid is None:
+            self.full_clean()
+        return self._is_valid
 
     def add_prefix(self, name):
         """
@@ -129,15 +142,29 @@
 
     def first_fieldset_attrs(self):
         "Returns attributes for first fieldset as HTML code."
-        if self.has_fieldsets() and 'attrs' in self._meta.fieldsets[0]:
-            return flatatt(self._meta.fieldsets[0]['attrs'])
+        if self.has_fieldsets():
+            if isinstance(self._meta.fieldsets[0], dict):
+                attrs = self._meta.fieldsets[0].get('attrs')
+            else:
+                attrs = self.inlines[0].fieldset_attrs
+        else:
+            attrs = None
+        if attrs:
+            return flatatt(attrs)
         else:
             return u''
 
     def first_fieldset_legend_tag(self):
         "Returns legend tag for first fieldset as HTML code."
-        if self.has_fieldsets() and 'legend' in self._meta.fieldsets[0]:
-            return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(self._meta.fieldsets[0]['legend'])))
+        if self.has_fieldsets():
+            if isinstance(self._meta.fieldsets[0], dict):
+                legend = self._meta.fieldsets[0].get('legend')
+            else:
+                legend = self.inlines[0].fieldset_legend
+        else:
+            legend = None
+        if legend:
+            return mark_safe(u'<legend>%s</legend>' % conditional_escape(force_unicode(legend)))
         else:
             return u''
 
@@ -205,6 +232,26 @@
             output.append(fieldset_end_html)
         return u'\n'.join(output)
 
+    def _inline_html_output(self, 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.fieldset_legend:
+                legend_tag = legend_tag_html % {
+                    'legend': conditional_escape(force_unicode(inline.fieldset_legend)),
+                }
+            if inline.fieldset_attrs:
+                attrs = flatatt(inline.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.hidden_row_class:
@@ -232,12 +279,17 @@
         if top_errors:
             output.append(self._top_errors_html_output(top_errors, top_errors_html))
         if self.has_fieldsets():
+            inlines = list(self.inlines) # Copy it - method pop should not changed self.inlines.
             for i, fieldset in enumerate(self._meta.fieldsets):
-                fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
                 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, dict):
+                    fields = dict((name, visible_fields[name]) for name in fieldset['fields'] if name in visible_fields)
+                    output.append(self._fieldset_html_output(fields, fieldset, is_first, is_last,
+                        fieldset_start_html, fieldset_end_html, legend_tag_html))
+                else:
+                    output.append(self._inline_html_output(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:
@@ -288,21 +340,47 @@
         }
         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 or inlines 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 non_field_errors(self):
         """
         Returns an ErrorList of errors that aren't associated with a particular
         field -- i.e., from Form.clean(). Returns an empty ErrorList if there
         are none.
         """
+        if self._is_valid is None:
+            self.full_clean()
         return self.errors.get(NON_FIELD_ERRORS, self.error_class())
 
     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.
+            self._is_valid = False
             return
         self.cleaned_data = {}
         # If the form is permitted to be empty, and none of the form data has
@@ -328,12 +406,20 @@
                 self._errors[name] = self.error_class(e.messages)
                 if name in self.cleaned_data:
                     del self.cleaned_data[name]
+                self._is_valid = False
         try:
             self.cleaned_data = self.clean()
         except ValidationError, e:
             self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
-        if self._errors:
+            self._is_valid = False
+        for inline in self.inlines:
+            inline.full_clean()
+            if not inline.is_valid():
+                self._is_valid = False 
+        if not self._is_valid:
             delattr(self, 'cleaned_data')
+            for inline in self.inlines:
+                inline._is_valid = False
 
     def clean(self):
         """
@@ -375,17 +461,23 @@
         media = Media()
         for field in self.fields.values():
             media = media + field.widget.media
+        for inline in self.inlines:
+            media = media + inline.media
         return media
     media = property(_get_media)
 
     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/forms/formsets.py'
--- django/forms/formsets.py	2008-07-24 12:10:12 +0000
+++ django/forms/formsets.py	2008-08-01 09:03:20 +0000
@@ -1,11 +1,12 @@
-from forms import Form
+from forms import Form, FormOptions
 from django.utils.encoding import StrAndUnicode
 from django.utils.safestring import mark_safe
 from fields import IntegerField, BooleanField
 from widgets import Media, HiddenInput
-from util import ErrorList, ValidationError
+from util import ValidationError
+import metaclassing
 
-__all__ = ('BaseFormSet', 'all_valid')
+__all__ = ('BaseFormSet', 'FormSet', 'formset_factory', 'all_valid')
 
 # special field names
 TOTAL_FORM_COUNT = 'TOTAL_FORMS'
@@ -24,21 +25,45 @@
         self.base_fields[INITIAL_FORM_COUNT] = 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.can_delete = getattr(options, 'can_delete', False)
+        self.can_order = getattr(options, 'can_order', False)
+        self.extra = getattr(options, 'extra', 1)
+        self.fieldset_attrs = getattr(options, 'fieldset_attrs', None)
+        self.fieldset_legend = getattr(options, 'fieldset_legend', None)
+        self.max_num = getattr(options, 'max_num', 0)
+        self.output_type = getattr(options, 'output_type', 'tr')
+        # self.output_type = getattr(options, 'output_type', 'original_table') # backward-compatible 
+
+class FormSetMetaclass(type):
+    def __new__(cls, name, bases, attrs):
+        new_class = type.__new__(cls, name, bases, attrs)
+        metaclassing.create_meta(new_class, attrs)
+        metaclassing.create_form_if_not_exists(new_class, attrs)
+        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
+        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, error_class=ErrorList):
+                 initial=None, error_class=None):
         self.is_bound = data is not None or files is not None
         self.prefix = prefix or 'form'
         self.auto_id = auto_id
         self.data = data
         self.files = files
         self.initial = initial
-        self.error_class = error_class
-        self._errors = None
-        self._non_form_errors = None
+        if error_class is not None:
+            self.error_class = error_class
+        self._is_valid = None # Stores validation state after full_clean() has been called.
         # initialization is different depending on whether we recieved data, initial, or nothing
         if data or files:
             self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
@@ -66,7 +91,7 @@
         self._construct_forms()
 
     def __unicode__(self):
-        return self.as_table()
+        return getattr(self, 'as_%s' % self.output_type)()
 
     def _construct_forms(self):
         # instantiate all the forms and put them in self.forms
@@ -181,15 +206,15 @@
         form -- i.e., from formset.clean(). Returns an empty ErrorList if there
         are none.
         """
-        if self._non_form_errors is not None:
-            return self._non_form_errors
-        return self.error_class()
+        if self._is_valid is None:
+            self.full_clean()
+        return self._non_form_errors
 
     def _get_errors(self):
         """
         Returns a list of form.errors for every form in self.forms.
         """
-        if self._errors is None:
+        if self._is_valid is None:
             self.full_clean()
         return self._errors
     errors = property(_get_errors)
@@ -198,31 +223,31 @@
         """
         Returns True if form.errors is empty for every form in self.forms.
         """
-        if not self.is_bound:
-            return False
-        # We loop over every form.errors here rather than short circuiting on the
-        # first failure to make sure validation gets triggered for every form.
-        forms_valid = True
-        for errors in self.errors:
-            if bool(errors):
-                forms_valid = False
-        return forms_valid and not bool(self.non_form_errors())
+        if self._is_valid is None:
+            self.full_clean()
+        return self._is_valid
 
     def full_clean(self):
         """
         Cleans all of self.data and populates self._errors.
         """
+        self._is_valid = True # Assume the form is valid until proven otherwise.
         self._errors = []
+        self._non_form_errors = self.error_class()
         if not self.is_bound: # Stop further processing.
+            self._is_valid = False
             return
         for i in range(0, self._total_form_count):
             form = self.forms[i]
             self._errors.append(form.errors)
+            if form.errors:
+                self._is_valid = False
         # Give self.clean() a chance to do cross-form validation.
         try:
             self.clean()
         except ValidationError, e:
-            self._non_form_errors = e.messages
+            self._non_form_errors = self.error_class(e.messages)
+            self._is_valid = False
 
     def clean(self):
         """
@@ -264,6 +289,39 @@
     media = property(_get_media)
 
     def as_table(self):
+        "Returns this form rendered as HTML <tr>s."
+        return self._html_output_non_form_errors() + u'\n'.join(u'<table>\n%s\n</table>' % form.as_table() for form in [self.management_form] + self.forms)
+
+    def as_ul(self):
+        "Returns this form rendered as HTML <li>s."
+        return self._html_output_non_form_errors() + u'\n'.join(u'<ul>\n%s\n</ul>' % form.as_ul() for form in [self.management_form] + self.forms)
+
+    def as_p(self):
+        "Returns this form rendered as HTML <p>s."
+        return self._html_output_non_form_errors() + u'\n'.join(u'<div>\n%s\n</div>' % form.as_p() for form in [self.management_form] + self.forms)
+
+    def as_tr(self):
+        "Returns this form rendered as HTML <td>s."
+        output = [self.management_form.as_tr()]
+        if self.non_form_errors:
+            output.append(u'<tr><td colspan="%s">%s</td></tr>' % (
+                len([bf for bf in self.forms[0] if not bf.is_hidden]),
+                self._html_output_non_form_errors(),
+            ))  
+        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 _html_output_non_form_errors(self):
+        if self.non_form_errors:
+            return u'<div>%s</div>' % unicode(self.non_form_errors())
+        else:
+            return u''
+
+    def as_original_table(self):
         "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
         # XXX: there is no semantic division between forms here, there
         # probably should be. It might make sense to render each form as a
@@ -271,13 +329,17 @@
         forms = u' '.join([form.as_table() for form in self.forms])
         return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
 
-def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
-                    can_delete=False, max_num=0):
+class FormSet(BaseFormSet):
+    __metaclass__ = FormSetMetaclass
+    _options = FormSetOptions
+
+def formset_factory(form, formset=FormSet, extra=1, can_order=False,
+                    can_delete=False, max_num=0, **kwargs):
     """Return a FormSet for the given form class."""
-    attrs = {'form': form, 'extra': extra,
-             'can_order': can_order, 'can_delete': can_delete,
-             'max_num': max_num}
-    return type(form.__name__ + 'FormSet', (formset,), attrs)
+    kwargs.update(locals())
+    meta_class = type('Meta', (), kwargs)
+    bases = (formset == FormSet and (FormSet,) or (formset, FormSet))
+    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 
 
 def all_valid(formsets):
     """Returns true if every formset in formsets is valid."""

=== modified file 'django/forms/metaclassing.py'
--- django/forms/metaclassing.py	2008-08-01 08:59:48 +0000
+++ django/forms/metaclassing.py	2008-08-01 09:03:19 +0000
@@ -11,7 +11,7 @@
 def create_meta(cls, attrs):
     cls._meta = cls._options(getattr(cls, 'Meta', None))
     for name, attr in cls._meta.__dict__.items():
-        if name not in ('fieldsets', 'fields', 'exclude'):
+        if name not in ('fieldsets', 'fields', 'exclude', 'inlines', 'base_form'):
             setattr(cls, name, attr)
 
 def create_declared_fields(cls, attrs):
@@ -60,7 +60,8 @@
     if cls._meta.fieldsets:
         names = []
         for fieldset in cls._meta.fieldsets:
-            names.extend(fieldset['fields'])
+            if isinstance(fieldset, dict):
+                names.extend(fieldset['fields'])
     elif cls._meta.fields:
         names = cls._meta.fields
     elif cls._meta.exclude:
@@ -72,3 +73,59 @@
 def create_media(cls, attrs):
     if not 'media' in attrs:
         cls.media = media_property(cls)
+
+def create_fieldsets_if_inlines_exist(cls, attrs):
+    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, attrs):
+    if not cls.form:
+        form_attrs = {
+            'Meta': type('Meta', (), cls._meta.__dict__),
+        }
+        for name, possible_field in attrs.items():
+            if isinstance(possible_field, Field):
+                form_attrs[name] = possible_field
+                delattr(cls, name)
+        cls.form = type(cls.__name__ + 'Form', (cls._meta.base_form,), form_attrs)
+
+def check_no_fieldsets_in_inner_form(cls, attrs):
+    if cls.form._meta.fieldsets:
+        raise ImproperlyConfigured("%s cannot have form with fieldsets." % cls.__name__)
+
+def add_fk_attribute_and_remove_fk_from_base_fields(cls, attrs):
+    # If models are not set, this class would not be used directly.
+    if not (cls.parent_model and cls.model):
+        return
+    # Try to discover what the foreign key from model to parent_model is.
+    fks_to_parent = []
+    for field in cls.model._meta.fields:
+        # Exceptions are neccessary here - ForeignKey cannot be imported for circular dependancy.
+        try:
+            if field.rel.to == cls.parent_model or field.rel.to in cls.parent_model._meta.parents.keys():
+                fks_to_parent.append(field)
+        except AttributeError:
+            pass
+    if cls.fk_name:
+        fks_to_parent = [fk for fk in fks_to_parent if fk.name == cls.fk_name]
+        if len(fks_to_parent) == 0:
+            raise ImproperlyConfigured("%s has no ForeignKey with name %s to %s." %
+                (cls.model, cls.fk_name, cls.parent_model))
+        elif len(fks_to_parent) > 1:
+            raise ImproperlyConfigured("%s has more than one ForeignKey with name %s to %s." %
+                (cls.model, cls.fk_name, cls.parent_model))
+    else:
+        if len(fks_to_parent) == 0:
+            raise ImproperlyConfigured("%s has no ForeignKey to %s." %
+                (cls.model, cls.parent_model))
+        if len(fks_to_parent) > 1:
+            raise ImproperlyConfigured("%s has more than one ForeignKey to %s." %
+                (cls.model, cls.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.form.base_fields[cls.fk.name]
+    except KeyError:
+        pass

=== modified file 'django/forms/models.py'
--- django/forms/models.py	2008-08-01 21:12:51 +0000
+++ django/forms/models.py	2008-08-01 21:12:56 +0000
@@ -11,15 +11,18 @@
 
 from util import ValidationError
 from forms import FormOptions, FormMetaclass, BaseForm
+from formsets import FormSetOptions, FormSetMetaclass
 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
-from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
+from formsets import BaseFormSet, DELETION_FIELD_NAME
 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', 'BaseModelForm',
+    'ModelForm', 'BaseInlineFormSet', 'InlineFormSet', 'modelform_factory',
+    'modelformset_factory', 'inlineformset_factory',
 )
 
 def save_instance(form, instance, fields=None, fail_message='saved',
@@ -219,6 +222,7 @@
         metaclassing.create_declared_fields(new_class, attrs)
         metaclassing.create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
         metaclassing.create_base_fields_from_base_fields_pool(new_class, attrs)
+        metaclassing.create_fieldsets_if_inlines_exist(new_class, attrs)
         metaclassing.create_media(new_class, attrs)
         return new_class
 
@@ -240,6 +244,16 @@
         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
                                             error_class, label_suffix, empty_permitted)
 
+    def _construct_inlines(self):
+        # this class can create inlines which are subclass of BaseInlineFormSet
+        self.inlines = []
+        if self.has_fieldsets():
+            for fieldset in self._meta.fieldsets:
+                if not isinstance(fieldset, dict):
+                    if not issubclass(fieldset, BaseInlineFormSet):
+                        raise ValueError('%s cannot create instance of %s.' % (self.__class__.__name__, fieldset.__name__))
+                    self.inlines.append(fieldset(self.data, self.files, self.instance))
+
     def save(self, commit=True):
         """
         Saves this ``form``'s cleaned_data into model instance
@@ -252,28 +266,31 @@
             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
 
 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
-                       formfield_callback=lambda f: f.formfield()):
-    # HACK: we should be able to construct a ModelForm without creating
-    # and passing in a temporary inner class
-    class Meta:
-        pass
-    setattr(Meta, 'model', model)
-    setattr(Meta, 'fields', fields)
-    setattr(Meta, 'exclude', exclude)
-    class_name = model.__name__ + 'Form'
-    return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, 
-                              'formfield_callback': formfield_callback})
+                       formfield_callback=lambda f: f.formfield(), **kwargs):
+    kwargs.update(locals())
+    meta_class = type('Meta', (), kwargs) 
+    bases = (form == ModelForm and (ModelForm,) or (form, ModelForm))
+    return ModelFormMetaclass(model.__name__ + 'Form', bases,
+        {'Meta': meta_class, 'formfield_callback': formfield_callback})
 
 
 # ModelFormSets ##############################################################
 
+class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
+    def __init__(self, options=None):
+        super(ModelFormSetOptions, self).__init__(options)
+        # options changed compared to superclass
+        self.base_form = getattr(options, 'base_form', ModelForm)
+
 class BaseModelFormSet(BaseFormSet):
     """
     A ``FormSet`` for editing a queryset and/or adding new objects to it.
@@ -362,24 +379,45 @@
             form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
         super(BaseModelFormSet, self).add_fields(form, index)
 
+class ModelFormSet(BaseModelFormSet):
+    __metaclass__ = FormSetMetaclass # no changes are needed
+    _options =  ModelFormSetOptions
+
 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
-                         formset=BaseModelFormSet,
+                         formset=ModelFormSet,
                          extra=1, can_delete=False, can_order=False,
-                         max_num=0, fields=None, exclude=None):
+                         max_num=0, fields=None, exclude=None, **kwargs):
     """
     Returns a FormSet class for the given Django model class.
     """
-    form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
-                             formfield_callback=formfield_callback)
-    FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
-                              can_order=can_order, can_delete=can_delete)
-    FormSet.model = model
-    return FormSet
+    kwargs.update(locals())
+    kwargs['form'] = modelform_factory(**kwargs)
+    meta_class = type('Meta', (), kwargs)
+    bases = (formset == ModelFormSet and (ModelFormSet,) or (formset, ModelFormSet))
+    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 
 
 
 # InlineFormSets #############################################################
 
-class BaseInlineFormset(BaseModelFormSet):
+class InlineFormSetOptions(ModelFormSetOptions):
+    def __init__(self, options=None):
+        super(InlineFormSetOptions, self).__init__(options)
+        self.parent_model = getattr(options, 'parent_model', None)
+        self.fk_name = getattr(options, 'fk_name', None)
+        # options changed compared to superclass
+        self.can_delete = getattr(options, 'can_delete', True)
+        self.extra = getattr(options, 'extra', 3)
+
+class InlineFormSetMetaclass(FormSetMetaclass):
+    def __new__(cls, name, bases, attrs):
+        new_class = type.__new__(cls, name, bases, attrs)
+        metaclassing.create_meta(new_class, attrs)
+        metaclassing.create_form_if_not_exists(new_class, attrs)
+        metaclassing.check_no_fieldsets_in_inner_form(new_class, attrs)
+        metaclassing.add_fk_attribute_and_remove_fk_from_base_fields(new_class, attrs)
+        return new_class
+
+class BaseInlineFormSet(BaseModelFormSet):
     """A formset for child objects related to a parent."""
     def __init__(self, data=None, files=None, instance=None,
                  save_as_new=False, prefix=None):
@@ -388,13 +426,13 @@
         self.save_as_new = save_as_new
         # is there a better way to get the object descriptor?
         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
-        super(BaseInlineFormset, self).__init__(data, files, prefix=prefix or self.rel_name)
+        super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name)
     
     def _construct_forms(self):
         if self.save_as_new:
             self._total_form_count = self._initial_form_count
             self._initial_form_count = 0
-        super(BaseInlineFormset, self)._construct_forms()
+        super(BaseInlineFormSet, self)._construct_forms()
 
     def get_queryset(self):
         """
@@ -409,66 +447,26 @@
         new_obj = self.model(**kwargs)
         return save_instance(form, new_obj, commit=commit)
 
-def _get_foreign_key(parent_model, model, fk_name=None):
-    """
-    Finds and returns the ForeignKey from model to parent if there is one.
-    If fk_name is provided, assume it is the name of the ForeignKey field.
-    """
-    # avoid circular import
-    from django.db.models import ForeignKey
-    opts = model._meta
-    if fk_name:
-        fks_to_parent = [f for f in opts.fields if f.name == fk_name]
-        if len(fks_to_parent) == 1:
-            fk = fks_to_parent[0]
-            if not isinstance(fk, ForeignKey) or \
-                    (fk.rel.to != parent_model and 
-                     fk.rel.to not in parent_model._meta.parents.keys()):
-                raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
-        elif len(fks_to_parent) == 0:
-            raise Exception("%s has no field named '%s'" % (model, fk_name))
-    else:
-        # Try to discover what the ForeignKey from model to parent_model is
-        fks_to_parent = [
-            f for f in opts.fields 
-            if isinstance(f, ForeignKey) 
-            and (f.rel.to == parent_model 
-                or f.rel.to in parent_model._meta.parents.keys())
-        ]
-        if len(fks_to_parent) == 1:
-            fk = fks_to_parent[0]
-        elif len(fks_to_parent) == 0:
-            raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
-        else:
-            raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
-    return fk
-
+class InlineFormSet(BaseInlineFormSet):
+    __metaclass__ = InlineFormSetMetaclass
+    _options = InlineFormSetOptions
 
 def inlineformset_factory(parent_model, model, form=ModelForm,
-                          formset=BaseInlineFormset, fk_name=None,
+                          formset=InlineFormSet, fk_name=None,
                           fields=None, exclude=None,
                           extra=3, can_order=False, can_delete=True, max_num=0,
-                          formfield_callback=lambda f: f.formfield()):
+                          formfield_callback=lambda f: f.formfield(), **kwargs):
     """
     Returns an ``InlineFormset`` for the given kwargs.
 
     You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
     to ``parent_model``.
     """
-    fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
-    # let the formset handle object deletion by default
-    
-    if exclude is not None:
-        exclude.append(fk.name)
-    else:
-        exclude = [fk.name]
-    FormSet = modelformset_factory(model, form=form,
-                                    formfield_callback=formfield_callback,
-                                    formset=formset,
-                                    extra=extra, can_delete=can_delete, can_order=can_order,
-                                    fields=fields, exclude=exclude, max_num=max_num)
-    FormSet.fk = fk
-    return FormSet
+    kwargs.update(locals())
+    kwargs['form'] = modelform_factory(**kwargs)
+    meta_class = type('Meta', (), kwargs)
+    bases = (formset == InlineFormSet and (InlineFormSet,) or (form, InlineFormSet))
+    return type(form.__name__ + 'FormSet', bases, {'Meta': meta_class}) 
 
 
 # Fields #####################################################################

=== modified file 'tests/regressiontests/inline_formsets/models.py'
--- tests/regressiontests/inline_formsets/models.py	2008-07-20 21:58:25 +0000
+++ tests/regressiontests/inline_formsets/models.py	2008-07-31 10:28:23 +0000
@@ -24,7 +24,7 @@
 >>> ifs = inlineformset_factory(Parent, Child)
 Traceback (most recent call last):
     ...
-Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
+ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has more than one ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>.
 
 
 These two should both work without a problem.
@@ -39,7 +39,7 @@
 >>> ifs = inlineformset_factory(Parent, Child, fk_name='school')
 Traceback (most recent call last):
     ...
-Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
+ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name school to <class 'regressiontests.inline_formsets.models.Parent'>.
 
 
 If the field specified in fk_name is not a ForeignKey, we should get an
@@ -48,7 +48,7 @@
 >>> ifs = inlineformset_factory(Parent, Child, fk_name='test')
 Traceback (most recent call last):
     ...
-Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
+ImproperlyConfigured: <class 'regressiontests.inline_formsets.models.Child'> has no ForeignKey with name test to <class 'regressiontests.inline_formsets.models.Parent'>.
 
 
 """

