diff --git a/AUTHORS b/AUTHORS
index 8cb71c1..fae6f81 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -412,6 +412,7 @@ answer newbie questions, and generally made Django that much better:
     Luciano Ramalho
     Amit Ramon <amit.ramon@gmail.com>
     Philippe Raoult <philippe.raoult@n2nsoft.com>
+    Iván Raskovsky <raskovsky@gmail.com>
     Massimiliano Ravelli <massimiliano.ravelli@gmail.com>
     Brian Ray <http://brianray.chipy.org/>
     Łukasz Rekucki <lrekucki@gmail.com>
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index f052fe1..9a25ace 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -25,6 +25,7 @@ from django.utils.text import capfirst, get_text_list
 from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 from django.utils.encoding import force_unicode
+from django.views.generic import EnhancedInlineFormSet
 
 HORIZONTAL, VERTICAL = 1, 2
 # returns the <ul> class for a given radio_admin field
@@ -1293,7 +1294,7 @@ class ModelAdmin(BaseModelAdmin):
             "admin/object_history.html"
         ], context, current_app=self.admin_site.name)
 
-class InlineModelAdmin(BaseModelAdmin):
+class InlineModelAdmin(BaseModelAdmin, EnhancedInlineFormSet):
     """
     Options for inline editing of ``model`` instances.
 
@@ -1301,15 +1302,11 @@ class InlineModelAdmin(BaseModelAdmin):
     ``model`` to its parent. This is required if ``model`` has more than one
     ``ForeignKey`` to its parent.
     """
-    model = None
-    fk_name = None
     formset = BaseInlineFormSet
-    extra = 3
-    max_num = None
     template = None
     verbose_name = None
     verbose_name_plural = None
-    can_delete = True
+    can_delete = True # True in EnhancedInlineFormSet
 
     def __init__(self, parent_model, admin_site):
         self.admin_site = admin_site
@@ -1334,32 +1331,19 @@ class InlineModelAdmin(BaseModelAdmin):
 
     def get_formset(self, request, obj=None, **kwargs):
         """Returns a BaseInlineFormSet class for use in admin add/change views."""
-        if self.declared_fieldsets:
-            fields = flatten_fieldsets(self.declared_fieldsets)
-        else:
-            fields = None
-        if self.exclude is None:
-            exclude = []
-        else:
-            exclude = list(self.exclude)
-        exclude.extend(kwargs.get("exclude", []))
+
+        exclude = self.get_exclude() or []
         exclude.extend(self.get_readonly_fields(request, obj))
-        # if exclude is an empty list we use None, since that's the actual
-        # default
         exclude = exclude or None
-        defaults = {
-            "form": self.form,
-            "formset": self.formset,
-            "fk_name": self.fk_name,
-            "fields": fields,
+
+        new_kwargs = {
             "exclude": exclude,
             "formfield_callback": partial(self.formfield_for_dbfield, request=request),
-            "extra": self.extra,
-            "max_num": self.max_num,
-            "can_delete": self.can_delete,
+            "parent_model": self.parent_model,
         }
-        defaults.update(kwargs)
-        return inlineformset_factory(self.parent_model, self.model, **defaults)
+        new_kwargs.update(kwargs)
+        
+        return self.get_base_formset(**new_kwargs)
 
     def get_fieldsets(self, request, obj=None):
         if self.declared_fieldsets:
@@ -1368,6 +1352,19 @@ class InlineModelAdmin(BaseModelAdmin):
         fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
         return [(None, {'fields': fields})]
 
+    def get_fields(self):
+        if self.declared_fieldsets:
+            return flatten_fieldsets(self.declared_fieldsets)
+        else:
+            return self.fields
+
+    # Stuff due to different naming
+    def get_formset_class(self):
+        return self.formset
+
+    def get_form_class(self):
+        return self.form
+
 class StackedInline(InlineModelAdmin):
     template = 'admin/edit_inline/stacked.html'
 
diff --git a/django/views/generic/__init__.py b/django/views/generic/__init__.py
index 1a98067..c16ba52 100644
--- a/django/views/generic/__init__.py
+++ b/django/views/generic/__init__.py
@@ -6,6 +6,10 @@ from django.views.generic.detail import DetailView
 from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
 from django.views.generic.list import ListView
 
+from django.views.generic.formsets import (FormSetView, ModelFormSetView,
+                               InlineFormSetView, EnhancedFormSet,
+                               EnhancedModelFormSet, EnhancedInlineFormSet, )
+
 
 class GenericViewError(Exception):
     """A problem in a generic view."""
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
deleted file mode 100644
index 3cade52..0000000
--- a/django/views/generic/edit.py
+++ /dev/null
@@ -1,242 +0,0 @@
-from django.forms import models as model_forms
-from django.core.exceptions import ImproperlyConfigured
-from django.http import HttpResponseRedirect
-from django.views.generic.base import TemplateResponseMixin, View
-from django.views.generic.detail import (SingleObjectMixin,
-                        SingleObjectTemplateResponseMixin, BaseDetailView)
-
-
-class FormMixin(object):
-    """
-    A mixin that provides a way to show and handle a form in a request.
-    """
-
-    initial = {}
-    form_class = None
-    success_url = None
-
-    def get_initial(self):
-        """
-        Returns the initial data to use for forms on this view.
-        """
-        return self.initial
-
-    def get_form_class(self):
-        """
-        Returns the form class to use in this view
-        """
-        return self.form_class
-
-    def get_form(self, form_class):
-        """
-        Returns an instance of the form to be used in this view.
-        """
-        return form_class(**self.get_form_kwargs())
-
-    def get_form_kwargs(self):
-        """
-        Returns the keyword arguments for instanciating the form.
-        """
-        kwargs = {'initial': self.get_initial()}
-        if self.request.method in ('POST', 'PUT'):
-            kwargs.update({
-                'data': self.request.POST,
-                'files': self.request.FILES,
-            })
-        return kwargs
-
-    def get_context_data(self, **kwargs):
-        return kwargs
-
-    def get_success_url(self):
-        if self.success_url:
-            url = self.success_url
-        else:
-            raise ImproperlyConfigured(
-                "No URL to redirect to. Provide a success_url.")
-        return url
-
-    def form_valid(self, form):
-        return HttpResponseRedirect(self.get_success_url())
-
-    def form_invalid(self, form):
-        return self.render_to_response(self.get_context_data(form=form))
-
-
-class ModelFormMixin(FormMixin, SingleObjectMixin):
-    """
-    A mixin that provides a way to show and handle a modelform in a request.
-    """
-
-    def get_form_class(self):
-        """
-        Returns the form class to use in this view
-        """
-        if self.form_class:
-            return self.form_class
-        else:
-            if self.model is not None:
-                # If a model has been explicitly provided, use it
-                model = self.model
-            elif hasattr(self, 'object') and self.object is not None:
-                # If this view is operating on a single object, use
-                # the class of that object
-                model = self.object.__class__
-            else:
-                # Try to get a queryset and extract the model class
-                # from that
-                model = self.get_queryset().model
-            return model_forms.modelform_factory(model)
-
-    def get_form_kwargs(self):
-        """
-        Returns the keyword arguments for instanciating the form.
-        """
-        kwargs = super(ModelFormMixin, self).get_form_kwargs()
-        kwargs.update({'instance': self.object})
-        return kwargs
-
-    def get_success_url(self):
-        if self.success_url:
-            url = self.success_url % self.object.__dict__
-        else:
-            try:
-                url = self.object.get_absolute_url()
-            except AttributeError:
-                raise ImproperlyConfigured(
-                    "No URL to redirect to.  Either provide a url or define"
-                    " a get_absolute_url method on the Model.")
-        return url
-
-    def form_valid(self, form):
-        self.object = form.save()
-        return super(ModelFormMixin, self).form_valid(form)
-
-    def get_context_data(self, **kwargs):
-        context = kwargs
-        if self.object:
-            context['object'] = self.object
-            context_object_name = self.get_context_object_name(self.object)
-            if context_object_name:
-                context[context_object_name] = self.object
-        return context
-
-
-class ProcessFormView(View):
-    """
-    A mixin that processes a form on POST.
-    """
-    def get(self, request, *args, **kwargs):
-        form_class = self.get_form_class()
-        form = self.get_form(form_class)
-        return self.render_to_response(self.get_context_data(form=form))
-
-    def post(self, request, *args, **kwargs):
-        form_class = self.get_form_class()
-        form = self.get_form(form_class)
-        if form.is_valid():
-            return self.form_valid(form)
-        else:
-            return self.form_invalid(form)
-
-    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
-    # object, note that browsers only support POST for now.
-    def put(self, *args, **kwargs):
-        return self.post(*args, **kwargs)
-
-
-class BaseFormView(FormMixin, ProcessFormView):
-    """
-    A base view for displaying a form
-    """
-
-
-class FormView(TemplateResponseMixin, BaseFormView):
-    """
-    A view for displaying a form, and rendering a template response.
-    """
-
-
-class BaseCreateView(ModelFormMixin, ProcessFormView):
-    """
-    Base view for creating an new object instance.
-
-    Using this base class requires subclassing to provide a response mixin.
-    """
-    def get(self, request, *args, **kwargs):
-        self.object = None
-        return super(BaseCreateView, self).get(request, *args, **kwargs)
-
-    def post(self, request, *args, **kwargs):
-        self.object = None
-        return super(BaseCreateView, self).post(request, *args, **kwargs)
-
-
-class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
-    """
-    View for creating an new object instance,
-    with a response rendered by template.
-    """
-    template_name_suffix = '_form'
-
-
-class BaseUpdateView(ModelFormMixin, ProcessFormView):
-    """
-    Base view for updating an existing object.
-
-    Using this base class requires subclassing to provide a response mixin.
-    """
-    def get(self, request, *args, **kwargs):
-        self.object = self.get_object()
-        return super(BaseUpdateView, self).get(request, *args, **kwargs)
-
-    def post(self, request, *args, **kwargs):
-        self.object = self.get_object()
-        return super(BaseUpdateView, self).post(request, *args, **kwargs)
-
-
-class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
-    """
-    View for updating an object,
-    with a response rendered by template..
-    """
-    template_name_suffix = '_form'
-
-
-class DeletionMixin(object):
-    """
-    A mixin providing the ability to delete objects
-    """
-    success_url = None
-
-    def delete(self, request, *args, **kwargs):
-        self.object = self.get_object()
-        self.object.delete()
-        return HttpResponseRedirect(self.get_success_url())
-
-    # Add support for browsers which only accept GET and POST for now.
-    def post(self, *args, **kwargs):
-        return self.delete(*args, **kwargs)
-
-    def get_success_url(self):
-        if self.success_url:
-            return self.success_url
-        else:
-            raise ImproperlyConfigured(
-                "No URL to redirect to. Provide a success_url.")
-
-
-class BaseDeleteView(DeletionMixin, BaseDetailView):
-    """
-    Base view for deleting an object.
-
-    Using this base class requires subclassing to provide a response mixin.
-    """
-
-
-class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
-    """
-    View for deleting an object retrieved with `self.get_object()`,
-    with a response rendered by template.
-    """
-    template_name_suffix = '_confirm_delete'
diff --git a/django/views/generic/edit/__init__.py b/django/views/generic/edit/__init__.py
new file mode 100644
index 0000000..3567224
--- /dev/null
+++ b/django/views/generic/edit/__init__.py
@@ -0,0 +1,65 @@
+from django.views.generic.detail import SingleObjectTemplateResponseMixin
+from django.views.generic.base import TemplateResponseMixin
+from django.views.generic.edit.base import (BaseFormView, BaseCreateView,
+                                BaseUpdateView, BaseDeleteView, BaseFormSetView,
+                                BaseModelFormSetView, BaseInlineFormSetView, )
+from django.views.generic.edit.forms import (FormMixin, ModelFormMixin,
+                                    ProcessFormView, DeletionMixin, )
+from django.views.generic.edit.formset import (EnhancedFormSet,
+                                   EnhancedModelFormSet, EnhancedInlineFormSet,
+                                   FormSetMixin, ModelFormSetMixin,
+                                   InlineFormSetMixin, ProcessFormSetView,
+                                   ProcessInlineFormSetView, )
+
+
+class FormView(TemplateResponseMixin, BaseFormView):
+    """
+    A view for displaying a form, and rendering a template response.
+    """
+
+
+class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
+    """
+    View for creating an new object instance,
+    with a response rendered by template.
+    """
+    template_name_suffix = '_form'
+
+
+class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
+    """
+    View for updating an object,
+    with a response rendered by template..
+    """
+    template_name_suffix = '_form'
+
+
+
+class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
+    """
+    View for deleting an object retrieved with `self.get_object()`,
+    with a response rendered by template.
+    """
+    template_name_suffix = '_confirm_delete'
+
+
+class FormSetView(TemplateResponseMixin, BaseFormSetView):
+    """
+    A view for displaying formsets, and rendering a template response
+    """
+
+
+class ModelFormSetView(TemplateResponseMixin, BaseModelFormSetView):
+    """
+    A view for displaying model formsets, and rendering a template response
+    """
+
+
+class InlineFormSetView(SingleObjectTemplateResponseMixin,
+                         BaseInlineFormSetView):
+    """
+    A view for displaying a model instance with it's inline formsets, and
+    rendering a template response
+    """
+    template_name_suffix = '_form'
+
diff --git a/django/views/generic/edit/base.py b/django/views/generic/edit/base.py
new file mode 100644
index 0000000..43fc2eb
--- /dev/null
+++ b/django/views/generic/edit/base.py
@@ -0,0 +1,69 @@
+from django.views.generic.detail import BaseDetailView
+from django.views.generic.base import View
+from django.views.generic.edit.forms import (FormMixin, ModelFormMixin,
+                                             ProcessFormView, DeletionMixin, )
+from django.views.generic.edit.formset import (FormSetMixin, ModelFormSetMixin,
+                                        InlineFormSetMixin, ProcessFormSetView,
+                                        ProcessInlineFormSetView, )
+
+
+class BaseFormView(FormMixin, ProcessFormView):
+    """
+    A base view for displaying a form
+    """
+
+
+class BaseCreateView(ModelFormMixin, ProcessFormView):
+    """
+    Base view for creating an new object instance.
+
+    Using this base class requires subclassing to provide a response mixin.
+    """
+    def get(self, request, *args, **kwargs):
+        self.object = None
+        return super(BaseCreateView, self).get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        self.object = None
+        return super(BaseCreateView, self).post(request, *args, **kwargs)
+
+
+class BaseUpdateView(ModelFormMixin, ProcessFormView):
+    """
+    Base view for updating an existing object.
+
+    Using this base class requires subclassing to provide a response mixin.
+    """
+    def get(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        return super(BaseUpdateView, self).get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        return super(BaseUpdateView, self).post(request, *args, **kwargs)
+
+
+class BaseDeleteView(DeletionMixin, BaseDetailView):
+    """
+    Base view for deleting an object.
+
+    Using this base class requires subclassing to provide a response mixin.
+    """
+
+
+class BaseFormSetView(FormSetMixin, ProcessFormSetView):
+    """
+    A base view for displaying formsets
+    """
+
+
+class BaseModelFormSetView(ModelFormSetMixin, ProcessFormSetView):
+    """
+    A base view for displaying model formsets
+    """
+
+
+class BaseInlineFormSetView(InlineFormSetMixin, ProcessInlineFormSetView):
+    """
+    A base view for displaying a model instance with it's inline formsets
+    """
diff --git a/django/views/generic/edit/forms.py b/django/views/generic/edit/forms.py
new file mode 100644
index 0000000..50ad970
--- /dev/null
+++ b/django/views/generic/edit/forms.py
@@ -0,0 +1,167 @@
+from django.forms import models as model_forms
+from django.core.exceptions import ImproperlyConfigured
+from django.http import HttpResponseRedirect
+from django.views.generic.base import View
+from django.views.generic.detail import SingleObjectMixin
+
+
+class FormMixin(object):
+    """
+    A mixin that provides a way to show and handle a form in a request.
+    """
+
+    initial = {}
+    form_class = None
+    success_url = None
+
+    def get_initial(self):
+        """
+        Returns the initial data to use for forms on this view.
+        """
+        return self.initial
+
+    def get_form_class(self):
+        """
+        Returns the form class to use in this view
+        """
+        return self.form_class
+
+    def get_form(self, form_class):
+        """
+        Returns an instance of the form to be used in this view.
+        """
+        return form_class(**self.get_form_kwargs())
+
+    def get_form_kwargs(self):
+        """
+        Returns the keyword arguments for instanciating the form.
+        """
+        kwargs = {'initial': self.get_initial()}
+        if self.request.method in ('POST', 'PUT'):
+            kwargs.update({
+                'data': self.request.POST,
+                'files': self.request.FILES,
+            })
+        return kwargs
+
+    def get_context_data(self, **kwargs):
+        return kwargs
+
+    def get_success_url(self):
+        if self.success_url:
+            url = self.success_url
+        else:
+            raise ImproperlyConfigured(
+                "No URL to redirect to. Provide a success_url.")
+        return url
+
+    def form_valid(self, form):
+        return HttpResponseRedirect(self.get_success_url())
+
+    def form_invalid(self, form):
+        return self.render_to_response(self.get_context_data(form=form))
+
+
+class ModelFormMixin(FormMixin, SingleObjectMixin):
+    """
+    A mixin that provides a way to show and handle a modelform in a request.
+    """
+
+    def get_form_class(self):
+        """
+        Returns the form class to use in this view
+        """
+        if self.form_class:
+            return self.form_class
+        else:
+            if self.model is not None:
+                # If a model has been explicitly provided, use it
+                model = self.model
+            elif hasattr(self, 'object') and self.object is not None:
+                # If this view is operating on a single object, use
+                # the class of that object
+                model = self.object.__class__
+            else:
+                # Try to get a queryset and extract the model class
+                # from that
+                model = self.get_queryset().model
+            return model_forms.modelform_factory(model)
+
+    def get_form_kwargs(self):
+        """
+        Returns the keyword arguments for instanciating the form.
+        """
+        kwargs = super(ModelFormMixin, self).get_form_kwargs()
+        kwargs.update({'instance': self.object})
+        return kwargs
+
+    def get_success_url(self):
+        if self.success_url:
+            url = self.success_url % self.object.__dict__
+        else:
+            try:
+                url = self.object.get_absolute_url()
+            except AttributeError:
+                raise ImproperlyConfigured(
+                    "No URL to redirect to.  Either provide a url or define"
+                    " a get_absolute_url method on the Model.")
+        return url
+
+    def form_valid(self, form):
+        self.object = form.save()
+        return super(ModelFormMixin, self).form_valid(form)
+
+    def get_context_data(self, **kwargs):
+        context = kwargs
+        if self.object:
+            context['object'] = self.object
+            context_object_name = self.get_context_object_name(self.object)
+            if context_object_name:
+                context[context_object_name] = self.object
+        return context
+
+
+class ProcessFormView(View):
+    """
+    A mixin that processes a form on POST.
+    """
+    def get(self, request, *args, **kwargs):
+        form_class = self.get_form_class()
+        form = self.get_form(form_class)
+        return self.render_to_response(self.get_context_data(form=form))
+
+    def post(self, request, *args, **kwargs):
+        form_class = self.get_form_class()
+        form = self.get_form(form_class)
+        if form.is_valid():
+            return self.form_valid(form)
+        else:
+            return self.form_invalid(form)
+
+    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
+    # object, note that browsers only support POST for now.
+    def put(self, *args, **kwargs):
+        return self.post(*args, **kwargs)
+
+
+class DeletionMixin(object):
+    """
+    A mixin providing the ability to delete objects
+    """
+    success_url = None
+
+    def delete(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        self.object.delete()
+        return HttpResponseRedirect(self.get_success_url())
+
+    # Add support for browsers which only accept GET and POST for now.
+    def post(self, *args, **kwargs):
+        return self.delete(*args, **kwargs)
+
+    def get_success_url(self):
+        if self.success_url:
+            return self.success_url
+        else:
+            raise ImproperlyConfigured(
+                "No URL to redirect to. Provide a success_url.")
diff --git a/django/views/generic/edit/formset.py b/django/views/generic/edit/formset.py
new file mode 100644
index 0000000..4d8b2f8
--- /dev/null
+++ b/django/views/generic/edit/formset.py
@@ -0,0 +1,340 @@
+from django.http import HttpResponseRedirect
+from django.core.exceptions import ImproperlyConfigured
+from django.forms.formsets import formset_factory, BaseFormSet, all_valid
+from django.forms.models import (modelformset_factory, inlineformset_factory,
+                                 BaseModelFormSet, BaseInlineFormSet, ModelForm)
+from django.views.generic.edit.forms import ModelFormMixin
+from django.views.generic.base import View
+
+
+class EnhancedFormSet(object):
+    """
+    A base class for generic formsets
+    """
+
+    form_class = None
+    formset_class = BaseFormSet
+
+    # formset_factory kwargs
+    extra = 3
+    can_order = False
+    can_delete = False
+    max_num = None
+
+    def get_base_formset(self, **kwargs):
+        """
+        Returns the base formset
+        """
+        new_kwargs = self.get_kwargs()
+        new_kwargs.update(**kwargs)
+        return self.get_factory()(**new_kwargs)
+
+    def get_factory(self):
+        """
+        Returns the factory used to construct the formsets
+        """
+        return formset_factory
+
+    def get_form_class(self):
+        return self.form_class
+
+    def get_formset_class(self):
+        return self.formset_class
+
+    def get_kwargs(self):
+        return {'form': self.get_form_class(),
+                'formset': self.get_formset_class(),
+                'extra': self.extra,
+                'can_order': self.can_order,
+                'can_delete': self.can_delete,
+                'max_num': self.max_num, }
+
+
+class EnhancedModelFormSet(EnhancedFormSet):
+    """
+    A base class for generic model formsets
+    """
+    # TODO: provide a hook for formfield_callback
+
+    form_class = ModelForm
+    formset_class = BaseModelFormSet
+    model = None
+    queryset = None
+    fields = None
+    exclude = None
+
+    def get_factory(self):
+        return modelformset_factory
+
+    def get_model(self):
+        if self.model:
+            return self.model
+        else:
+            try:
+                return self.get_form_class().Meta.model
+            except AttributeError:
+                raise ImproperlyConfigured(
+                "No model to create the modelformset. Provide one.")
+
+    def get_queryset(self):
+        return self.queryset
+
+    def get_fields(self):
+        return self.fields
+
+    def get_exclude(self):
+        return self.exclude
+
+    def get_kwargs(self):
+        kwargs = super(EnhancedModelFormSet, self).get_kwargs()
+        kwargs.update({
+            'model': self.get_model(),
+            'fields': self.get_fields(),
+            'exclude': self.get_exclude(),
+        })
+        return kwargs
+
+
+class EnhancedInlineFormSet(EnhancedModelFormSet):
+    """
+    A base class for generic inline formsets
+    """
+
+    fk_name = None
+    formset_class = BaseInlineFormSet
+
+    def get_factory(self):
+        return inlineformset_factory
+
+    def get_fk_name(self):
+        return self.fk_name
+
+    def get_kwargs(self):
+        kwargs = super(EnhancedInlineFormSet, self).get_kwargs()
+        kwargs.update({
+            'fk_name': self.get_fk_name(),
+        })
+        return kwargs
+
+
+class FormSetMixin(object):
+    """
+    A mixin that provides a way to show and handle formsets
+    """
+
+    formsets = []  # must be a list of BaseGenericFormSet
+    success_url = None
+
+    def __init__(self, *args, **kwargs):
+        self.instantiate_enhanced_formsets()
+
+    def instantiate_enhanced_formsets(self):
+        """
+        Instantiates the enhanced formsets
+        """
+        self.enhanced_formsets_instances = []
+        for formset in self.formsets:
+            enhanced_formset_instance = formset()
+            self.enhanced_formsets_instances.append(enhanced_formset_instance)
+
+    def construct_formsets(self):
+        """
+        Constructs the formsets
+        """
+        self.formsets_instances = []
+
+        prefixes = {}
+        for enhanced_formset in self.enhanced_formsets_instances:
+            base_formset = enhanced_formset.get_base_formset(
+                **self.get_factory_kwargs())
+
+            # calculate prefix
+            prefix = base_formset.get_default_prefix()
+            prefixes[prefix] = prefixes.get(prefix, 0) + 1
+            if prefixes[prefix] != 1:
+                prefix = "%s-%s" % (prefix, prefixes[prefix])
+
+            self.formsets_instances.append(
+                base_formset(prefix=prefix, **self.get_formsets_kwargs(
+                    enhanced_formset))
+            )
+
+    def get_factory_kwargs(self):
+        """
+        Returns the keyword arguments for the formsets factory
+        """
+        return {}
+
+    def get_formsets_kwargs(self, enhanced_formset):
+        """"
+        Returns the keyword arguments for instanciating the formsets
+        """
+
+        # default kwargs
+        kwargs = {}
+
+        if self.request.method in ('POST', 'PUT'):
+            kwargs.update({
+                'data': self.request.POST,
+                'files': self.request.FILES,
+            })
+        return kwargs
+
+    def get_context_data(self, **kwargs):
+        context_data = {
+            'formsets': [formset for formset in self.formsets_instances],
+        }
+
+        context_data.update(kwargs)
+        return context_data
+
+    def get_success_url(self):
+        if self.success_url:
+            url = self.success_url
+        else:
+            raise ImproperlyConfigured(
+                "No URL to redirect to. Provide a success_url")
+        return url
+
+    def formsets_valid(self):
+        return HttpResponseRedirect(self.get_success_url())
+
+    def formsets_invalid(self):
+        return self.render_to_response(self.get_context_data())
+
+
+class ModelFormSetMixin(FormSetMixin):
+    """
+    A mixin that provides a way to show and handle model formsets
+    """
+
+    def get_formsets_kwargs(self, enhanced_formset):
+        """"
+        Returns the keyword arguments for instanciating the model formsets
+        """
+        kwargs = super(ModelFormSetMixin, self).get_formsets_kwargs(
+                                                    enhanced_formset)
+        kwargs.update({
+            'queryset': enhanced_formset.get_queryset()
+        })
+        return kwargs
+
+    def formsets_valid(self):
+        # FIXME: beware of m2m
+        for formset in self.formsets_instances:
+            formset.save()
+        return super(ModelFormSetMixin, self).formsets_valid()
+
+
+class InlineFormSetMixin(ModelFormSetMixin, ModelFormMixin):
+    """ 
+    A mixin that provides a way to show and handle a model with it's inline
+    formsets
+    """
+    def get_formsets_kwargs(self, enhanced_formset):
+        """"
+        Returns the keyword arguments for instanciating the inline formsets
+        """
+        kwargs = super(InlineFormSetMixin, self).get_formsets_kwargs(
+                                                    enhanced_formset)
+        kwargs.update({
+            'instance': self.object
+        })
+        return kwargs
+
+    def get_context_data(self, **kwargs):
+        """
+        Adds the context data from both parents
+        """
+        context_data = ModelFormSetMixin.get_context_data(self)
+        context_data.update(ModelFormMixin.get_context_data(self, **kwargs))
+        return context_data
+
+    def get_factory_kwargs(self):
+        """
+        Returns the keyword arguments for the formsets factory
+        """
+        return {
+            'parent_model': self.object.__class__,
+        }
+
+    def form_valid(self, form):
+        self.object.save()
+        form.save_m2m()
+        for formset in self.formsets_instances:
+            formset.save()
+
+        return HttpResponseRedirect(self.get_success_url())
+
+    def form_invalid(self, form):
+        return self.render_to_response(self.get_context_data(form=form))
+
+
+class ProcessFormSetView(View):
+    """
+    A mixin that processes formsets on POST
+    """
+    def get(self, request, *args, **kwargs):
+        self.construct_formsets()
+        return self.render_to_response(self.get_context_data())
+
+    def post(self, request, *args, **kwargs):
+        self.construct_formsets()
+        if all_valid(self.formsets_instances):
+            return self.formsets_valid()
+        else:
+            return self.formsets_invalid()
+
+    def put(self, request, *args, **kwargs):
+        return self.post(*args, **kwargs)
+
+
+class ProcessInlineFormSetView(View):
+    """
+    A mixin that processes a model instance and it's inline formsets on POST
+    """
+
+    def get(self, request, *args, **kwargs):
+        # Create or Update
+        try:
+            self.object = self.get_object()
+        except AttributeError:
+            self.object = self.model()
+
+        # ProcessFormView
+        form_class = self.get_form_class()
+        form = self.get_form(form_class)
+
+        # ProcessFormSetView
+        self.construct_formsets()
+
+        return self.render_to_response(self.get_context_data(form=form))
+
+    def post(self, request, *args, **kwargs):
+        # Create or Update
+        try:
+            self.object = self.get_object()
+        except AttributeError:
+            self.object = self.model()
+
+        # ProcessFormView
+        form_class = self.get_form_class()
+        form = self.get_form(form_class)
+
+        if form.is_valid():
+            self.object = form.save(commit=False)
+
+            # ProcessFormSetViewV
+            self.construct_formsets()
+
+            if all_valid(self.formsets_instances):
+                return self.form_valid(form)
+        else:
+            # ProcessFormSetViewV
+            self.construct_formsets()
+        return self.form_invalid(form)
+
+
+    def put(self, request, *args, **kwargs):
+        return self.post(*args, **kwargs)
+
diff --git a/tests/regressiontests/generic_views/forms.py b/tests/regressiontests/generic_views/forms.py
index 7200947..b6ee715 100644
--- a/tests/regressiontests/generic_views/forms.py
+++ b/tests/regressiontests/generic_views/forms.py
@@ -1,6 +1,9 @@
 from django import forms
+from django.forms.formsets import formset_factory
 
-from regressiontests.generic_views.models import Author
+from django.views.generic import (EnhancedFormSet, EnhancedModelFormSet,
+                                     EnhancedInlineFormSet, )
+from regressiontests.generic_views.models import Author, Article
 
 
 class AuthorForm(forms.ModelForm):
@@ -9,3 +12,29 @@ class AuthorForm(forms.ModelForm):
 
     class Meta:
         model = Author
+
+
+class ArticleForm(forms.ModelForm):
+    class Meta:
+        model = Article
+        exclude = ('author', )
+
+
+class ArticleEnhancedFormSet(EnhancedFormSet):
+    form_class = ArticleForm
+
+
+class AuthorEnhancedFormSet(EnhancedFormSet):
+    form_class = AuthorForm
+
+
+class ArticleEnhancedModelFormSet(EnhancedModelFormSet):
+    model = Article
+
+
+class AuthorEnhancedModelFormSet(EnhancedModelFormSet):
+    model = Author
+
+
+class ArticleEnhancedInlineFormSet(EnhancedInlineFormSet):
+    model = Article
diff --git a/tests/regressiontests/generic_views/formsets.py b/tests/regressiontests/generic_views/formsets.py
new file mode 100644
index 0000000..be39734
--- /dev/null
+++ b/tests/regressiontests/generic_views/formsets.py
@@ -0,0 +1,193 @@
+from django.test import TestCase
+from django.core.exceptions import ImproperlyConfigured
+from django.views.generic.formsets import (FormSetMixin, ModelFormSetMixin,
+        EnhancedModelFormSet, )
+from regressiontests.generic_views.models import Author, Article
+
+
+class FormSetViewTests(TestCase):
+    urls = 'regressiontests.generic_views.urls'
+
+    def setUp(self):
+        self.data = {
+            'form-TOTAL_FORMS': u'3',
+            'form-INITIAL_FORMS': u'0',
+            'form-MAX_NUM_FORMS': u'',
+            'form-0-title': u'',
+            'form-0-pubdate': u'',
+            'form-1-title': u'',
+            'form-1-pubdate': u'',
+            'form-2-title': u'',
+            'form-2-pubdate': u'',
+
+            'form-2-TOTAL_FORMS': u'3',
+            'form-2-INITIAL_FORMS': u'0',
+            'form-2-MAX_NUM_FORMS': u'',
+            'form-2-0-name': u'',
+            'form-2-0-slug': u'',
+            'form-2-1-name': u'',
+            'form-2-1-slug': u'',
+            'form-2-2-name': u'',
+            'form-2-2-slug': u'',
+        }
+
+    def test_get(self):
+        response = self.client.get('/edit/formsets/')
+        self.assertEqual(response.status_code, 200)
+
+    def test_empty_post(self):
+        response = self.client.post('/edit/formsets/', self.data)
+        self.assertEqual(response.status_code, 302)
+
+    def test_valid(self):
+        self.data.update({
+            'form-0-title': u'first title',
+            'form-0-pubdate': u'2011-01-13',
+            'form-1-title': u'second title',
+            'form-1-pubdate': u'2011-01-13',
+            'form-2-0-name': u'this is my name',
+            'form-2-0-slug': u'this-is-my-name',
+        })
+        response = self.client.post('/edit/formsets/', self.data)
+        self.assertEqual(response.status_code, 302)
+
+    def test_invalid(self):
+        self.data.update({
+            'form-0-title': u'first title',
+            'form-0-pubdate': u'',
+        })
+        response = self.client.post('/edit/formsets/', self.data)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'ERROR')
+
+
+class ModelFormSetTests(TestCase):
+    def test_no_model_no_form_class(self):
+        formset = EnhancedModelFormSet()
+        self.assertRaises(ImproperlyConfigured, formset.get_model)
+
+
+class ModelFormSetViewTests(TestCase):
+    urls = 'regressiontests.generic_views.urls'
+
+    def setUp(self):
+        self.data = {
+            'form-TOTAL_FORMS': u'3',
+            'form-INITIAL_FORMS': u'0',
+            'form-MAX_NUM_FORMS': u'',
+            'form-0-title': u'',
+            'form-0-pubdate': u'',
+            'form-1-title': u'',
+            'form-1-pubdate': u'',
+            'form-2-title': u'',
+            'form-2-pubdate': u'',
+
+            'form-2-TOTAL_FORMS': u'3',
+            'form-2-INITIAL_FORMS': u'0',
+            'form-2-MAX_NUM_FORMS': u'',
+            'form-2-0-name': u'',
+            'form-2-1-name': u'',
+            'form-2-2-name': u'',
+        }
+
+    def test_get(self):
+        response = self.client.get('/edit/modelformsets/')
+        self.assertEqual(response.status_code, 200)
+
+    def test_empty_post(self):
+        response = self.client.post('/edit/modelformsets/', self.data)
+        self.assertEqual(response.status_code, 302)
+
+    def test_valid(self):
+        self.data.update({
+            'form-0-title': u'first title',
+            'form-0-pubdate': u'2011-01-13',
+            'form-1-title': u'second title',
+            'form-1-pubdate': u'2011-01-13',
+            'form-2-0-name': u'this is my name',
+            'form-2-0-slug': u'this-is-my-name',
+        })
+        response = self.client.post('/edit/modelformsets/', self.data)
+        self.assertEqual(Article.objects.count(), 2)
+        self.assertEqual(Author.objects.count(), 1)
+        self.assertEqual(response.status_code, 302)
+
+    def test_invalid(self):
+        self.data.update({
+            'form-0-title': u'first title',
+            'form-0-pubdate': u'',
+        })
+        response = self.client.post('/edit/modelformsets/', self.data)
+        self.assertEqual(Article.objects.count(), 0)
+        self.assertEqual(Author.objects.count(), 0)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'ERROR')
+
+
+class InlineFormSetViewTests(TestCase):
+    urls = 'regressiontests.generic_views.urls'
+
+    def setUp(self):
+        self.formsetmgmt = {
+            'article_set-TOTAL_FORMS': u'3',
+            'article_set-INITIAL_FORMS': u'0',
+            'article_set-MAX_NUM_FORMS': u'',
+        }
+        self.formsetdata = {
+            'article_set-0-title': u'title1',
+            'article_set-0-pubdate': u'2011-01-26',
+            'article_set-1-title': u'title2',
+            'article_set-1-pubdate': u'2011-01-26',
+            'article_set-2-title': u'title3',
+            'article_set-2-pubdate': u'2011-01-26',
+        }
+        self.formdata = {
+            'name': u'this is my name',
+            'slug': u'this-is-my-name',
+        }
+
+    def test_get(self):
+        response = self.client.get('/edit/inlineformsets/')
+        self.assertEqual(response.status_code, 200)
+
+    def test_empty_post(self):
+        data = {
+            'article_set-TOTAL_FORMS': u'3',
+            'article_set-INITIAL_FORMS': u'0',
+            'article_set-MAX_NUM_FORMS': u'',
+            'article_set-0-title': u'',
+            'article_set-0-pubdate': u'',
+            'article_set-1-title': u'',
+            'article_set-1-pubdate': u'',
+            'article_set-2-title': u'',
+            'article_set-2-pubdate': u'',
+            'name': u'',
+        }
+        response = self.client.post('/edit/inlineformsets/', data)
+        self.assertEqual(response.status_code, 200)
+
+    def test_valid(self):
+        data = self.formdata
+        data.update(self.formsetdata)
+        data.update(self.formsetmgmt)
+        response = self.client.post('/edit/inlineformsets/', data)
+        self.assertEqual(Article.objects.count(), 3)
+        self.assertEqual(Author.objects.count(), 1)
+        self.assertEqual(response.status_code, 302)
+
+    def test_no_form(self):
+        data = self.formsetdata
+        data.update(self.formsetmgmt)
+        response = self.client.post('/edit/inlineformsets/', data)
+        self.assertEqual(Article.objects.count(), 0)
+        self.assertEqual(Author.objects.count(), 0)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'ERROR')
+
+    def test_no_formset(self):
+        data = self.formdata
+        data.update(self.formsetmgmt)
+        response = self.client.post('/edit/inlineformsets/', data)
+        self.assertEqual(Article.objects.count(), 0)
+        self.assertEqual(Author.objects.count(), 1)
+        self.assertEqual(response.status_code, 302)
diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
index 5445e24..01433fe 100644
--- a/tests/regressiontests/generic_views/models.py
+++ b/tests/regressiontests/generic_views/models.py
@@ -1,5 +1,6 @@
 from django.db import models
 
+
 class Artist(models.Model):
     name = models.CharField(max_length=100)
 
@@ -15,6 +16,7 @@ class Artist(models.Model):
     def get_absolute_url(self):
         return ('artist_detail', (), {'pk': self.id})
 
+
 class Author(models.Model):
     name = models.CharField(max_length=100)
     slug = models.SlugField()
@@ -25,6 +27,7 @@ class Author(models.Model):
     def __unicode__(self):
         return self.name
 
+
 class Book(models.Model):
     name = models.CharField(max_length=300)
     slug = models.SlugField()
@@ -38,6 +41,13 @@ class Book(models.Model):
     def __unicode__(self):
         return self.name
 
+
 class Page(models.Model):
     content = models.TextField()
     template = models.CharField(max_length=300)
+
+
+class Article(models.Model):
+    title = models.CharField(max_length=100)
+    pubdate = models.DateField()
+    author = models.ForeignKey(Author, blank=True, null=True)
diff --git a/tests/regressiontests/generic_views/templates/authors_articles.html b/tests/regressiontests/generic_views/templates/authors_articles.html
new file mode 100644
index 0000000..7c9eca3
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/authors_articles.html
@@ -0,0 +1,10 @@
+{% for formset in formsets %}
+    {% if formset.errors %}
+    ERROR
+    {{formset.errors}}
+    {% endif %}
+    {% for form in formset %}
+    {{form.as_p}}
+    {% endfor %}
+    {{formset.management_form}}
+{% endfor %}
diff --git a/tests/regressiontests/generic_views/tests.py b/tests/regressiontests/generic_views/tests.py
index a4010aa..7f4b79f 100644
--- a/tests/regressiontests/generic_views/tests.py
+++ b/tests/regressiontests/generic_views/tests.py
@@ -3,3 +3,4 @@ from regressiontests.generic_views.dates import ArchiveIndexViewTests, YearArchi
 from regressiontests.generic_views.detail import DetailViewTest
 from regressiontests.generic_views.edit import ModelFormMixinTests, CreateViewTests, UpdateViewTests, DeleteViewTests
 from regressiontests.generic_views.list import ListViewTests
+from regressiontests.generic_views.formsets import FormSetViewTests, ModelFormSetTests, ModelFormSetViewTests, InlineFormSetViewTests
diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py
index 067c1f6..e14a252 100644
--- a/tests/regressiontests/generic_views/urls.py
+++ b/tests/regressiontests/generic_views/urls.py
@@ -208,10 +208,17 @@ urlpatterns = patterns('',
         views.BookDetail.as_view(allow_future=True)),
     (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/nopk/$',
         views.BookDetail.as_view()),
-
     (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/byslug/(?P<slug>[\w-]+)/$',
         views.BookDetail.as_view()),
 
+    # FormSet
+    (r'^edit/formsets/$',
+        views.AuthorsArticlesView.as_view()),
+    (r'^edit/modelformsets/$',
+        views.AuthorsArticlesModelsView.as_view()),
+    (r'^edit/inlineformsets/$',
+        views.AuthorsInlinesView.as_view()),
+
     # Useful for testing redirects
     (r'^accounts/login/$',  'django.contrib.auth.views.login')
 )
diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py
index 0c8fd49..58fd9d5 100644
--- a/tests/regressiontests/generic_views/views.py
+++ b/tests/regressiontests/generic_views/views.py
@@ -5,8 +5,10 @@ from django.utils.decorators import method_decorator
 from django.views import generic
 
 from regressiontests.generic_views.models import Artist, Author, Book, Page
-from regressiontests.generic_views.forms import AuthorForm
-
+from regressiontests.generic_views.forms import (AuthorForm,
+        ArticleEnhancedFormSet, AuthorEnhancedFormSet,
+        ArticleEnhancedModelFormSet, AuthorEnhancedModelFormSet,
+        ArticleEnhancedInlineFormSet, )
 
 class CustomTemplateView(generic.TemplateView):
     template_name = 'generic_views/about.html'
@@ -177,3 +179,22 @@ class BookDetail(BookConfig, generic.DateDetailView):
 class AuthorGetQuerySetFormView(generic.edit.ModelFormMixin):
     def get_queryset(self):
         return Author.objects.all()
+
+
+class AuthorsArticlesView(generic.FormSetView):
+    formsets = [ArticleEnhancedFormSet, AuthorEnhancedFormSet, ]
+    template_name = 'authors_articles.html'
+    success_url = '/list/authors/'
+    
+
+class AuthorsArticlesModelsView(generic.ModelFormSetView):
+    formsets = [ArticleEnhancedModelFormSet, AuthorEnhancedModelFormSet, ]
+    template_name = 'authors_articles.html'
+    success_url = '/list/authors/'
+
+
+class AuthorsInlinesView(generic.InlineFormSetView):
+    formsets = [ArticleEnhancedInlineFormSet, ]
+    template_name = 'authors_articles.html'
+    success_url = '/list/authors/'
+    model = Author
