=== modified file 'django/forms/forms.py'
--- django/forms/forms.py	2008-10-04 15:17:16 +0000
+++ django/forms/forms.py	2008-10-04 15:20:05 +0000
@@ -28,6 +28,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)
 
 def create_declared_fields(cls, attrs):
     """
@@ -72,7 +73,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:
@@ -81,6 +83,12 @@
         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, 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)
+
 class FormMetaclass(type):
     """
     Metaclass that converts Field attributes to a dictionary called
@@ -94,6 +102,7 @@
         create_declared_fields(new_class, attrs)
         create_base_fields_pool_from_declared_fields(new_class, attrs)
         create_base_fields_from_base_fields_pool(new_class, attrs)
+        create_fieldsets_if_inlines_exist(new_class, attrs)
         if 'media' not in attrs:
             new_class.media = media_property(new_class)
         return new_class
@@ -115,7 +124,7 @@
         self.error_class = error_class
         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
@@ -124,6 +133,15 @@
         # 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()
@@ -145,16 +163,26 @@
     
     def fieldsets(self):
         if self.has_fieldsets():
+            fieldset_counter, formset_counter = 0, 0
             for fieldset in self._meta.fieldsets:
-                yield {
-                    'attrs': fieldset.get('attrs', {}),
-                    'legend': fieldset.get('legend', u''),
-                    'fields': [self[name] for name in fieldset['fields']],
-                } 
-    
+                if isinstance(fieldset, dict):
+                    yield {
+                        'order': fieldset_counter,  
+                        'attrs': fieldset.get('attrs', {}),
+                        'legend': fieldset.get('legend', u''),
+                        'fields': [self[name] for name in fieldset['fields']],
+                    }
+                    fieldset_counter += 1       
+                else:
+                    yield {
+                        'order': formset_counter,
+                        'formset': self.inlines[formset_counter]
+                    }
+                    formset_counter += 1 
+
     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)
@@ -164,7 +192,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, field_name):
         """
@@ -254,11 +284,13 @@
 
     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
@@ -284,12 +316,20 @@
                 self._errors[name] = 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] = 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):
         """
@@ -337,6 +377,8 @@
         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)
 
@@ -348,6 +390,9 @@
         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-10-02 02:46:12 +0000
+++ django/forms/formsets.py	2008-10-02 23:21:31 +0000
@@ -38,8 +38,7 @@
         self.files = files
         self.initial = initial
         self.error_class = error_class
-        self._errors = None
-        self._non_form_errors = None
+        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)
@@ -182,15 +181,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)
@@ -199,31 +198,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):
         """

=== modified file 'django/forms/models.py'
--- django/forms/models.py	2008-10-08 17:13:50 +0000
+++ django/forms/models.py	2008-10-08 17:15:33 +0000
@@ -11,6 +11,7 @@
 from util import ValidationError, ErrorList
 from forms import FormOptions, BaseForm, create_declared_fields
 from forms import create_base_fields_from_base_fields_pool
+from forms import create_fieldsets_if_inlines_exist
 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
 from widgets import media_property
@@ -60,6 +61,9 @@
                 continue
             if f.name in cleaned_data:
                 f.save_form_data(instance, cleaned_data[f.name])
+        if not commit:
+            for inline in form.inlines:
+                inline.save_m2m()
     if commit:
         # If we are committing, save the instance and the m2m data immediately.
         instance.save()
@@ -209,6 +213,7 @@
         create_declared_fields(new_class, attrs)
         create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
         create_base_fields_from_base_fields_pool(new_class, attrs)
+        create_fieldsets_if_inlines_exist(new_class, attrs)
         if 'media' not in attrs:
             new_class.media = media_property(new_class)
         return new_class
@@ -233,6 +238,16 @@
         self.validate_unique()
         return self.cleaned_data
 
+    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 validate_unique(self):
         from django.db.models.fields import FieldDoesNotExist
 
@@ -295,6 +310,7 @@
                         {'model_name': unicode(model_name),
                          'field_label': unicode(field_label)}
                     ])
+                    self._is_valid = False 
                 # unique_together
                 else:
                     field_labels = [self.fields[field_name].label for field_name in unique_check]
@@ -304,7 +320,8 @@
                         {'model_name': unicode(model_name),
                          'field_label': unicode(field_labels)}
                     )
-
+                    self._is_valid = False 
+                
                 # Mark these fields as needing to be removed from cleaned data
                 # later.
                 for field_name in unique_check:
@@ -329,13 +346,15 @@
             fail_message = 'created'
         else:
             fail_message = 'changed'
-        return save_instance(self, self.instance, self.fields.keys(), fail_message, commit)
+        self.saved_instance = save_instance(self, self.instance, self.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
 
 def modelform_factory(model, form=ModelForm, fields=None, exclude=None, fieldsets=None,
-                       formfield_callback=lambda f: f.formfield()):
+                       inlines=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:
@@ -344,6 +363,7 @@
     setattr(Meta, 'fieldsets', fieldsets)
     setattr(Meta, 'fields', fields)
     setattr(Meta, 'exclude', exclude)
+    setattr(Meta, 'inlines', inlines)
     class_name = model.__name__ + 'Form'
     return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
                               'formfield_callback': formfield_callback})
@@ -451,12 +471,12 @@
 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
                          formset=BaseModelFormSet,
                          extra=1, can_delete=False, can_order=False,
-                         max_num=0, fields=None, exclude=None, fieldsets=None):
+                         max_num=0, fields=None, exclude=None, fieldsets=None, inlines=None):
     """
     Returns a FormSet class for the given Django model class.
     """
     form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
-                             fieldsets=fieldsets,
+                             fieldsets=fieldsets, inlines=inlines,
                              formfield_callback=formfield_callback)
     FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
                               can_order=can_order, can_delete=can_delete)
@@ -546,7 +566,7 @@
 
 def inlineformset_factory(parent_model, model, form=ModelForm,
                           formset=BaseInlineFormSet, fk_name=None,
-                          fields=None, exclude=None, fieldsets=None,
+                          fields=None, exclude=None, fieldsets=None, inlines=None,
                           extra=3, can_order=False, can_delete=True, max_num=0,
                           formfield_callback=lambda f: f.formfield()):
     """
@@ -574,6 +594,7 @@
         'fieldsets': fieldsets,
         'fields': fields,
         'exclude': exclude,
+        'inlines': inlines,
         'max_num': max_num,
     }
     FormSet = modelformset_factory(model, **kwargs)

