diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index f4205f2..352cd8b 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -715,11 +715,16 @@ class ModelAdmin(BaseModelAdmin):
         """
         obj.delete()
 
-    def save_formset(self, request, form, formset, change):
+    def save_formset(self, request, formset, change):
         """
         Given an inline formset save it to the database.
         """
         formset.save()
+        for form in formset.forms:
+            if hasattr(form, 'nested_formsets'):
+                for nested_formset in form.nested_formsets:
+                    self.save_formset(request, nested_formset, change)
+                    
 
     def save_related(self, request, form, formsets, change):
         """
@@ -731,7 +736,7 @@ class ModelAdmin(BaseModelAdmin):
         """
         form.save_m2m()
         for formset in formsets:
-            self.save_formset(request, form, formset, change=change)
+            self.save_formset(request, formset, change=change)
 
     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
         opts = self.model._meta
@@ -920,6 +925,73 @@ class ModelAdmin(BaseModelAdmin):
             self.message_user(request, msg)
             return None
 
+    
+        
+    def add_nested_inline_formsets(self, request, inline, formset, depth=0):
+        if depth > 5:
+            raise Exception("Maximum nesting depth reached (5)")
+        for form in formset.forms:                
+            nested_formsets = []
+            for nested_inline in inline.get_inline_instances(request):
+                InlineFormSet = nested_inline.get_formset(request, form.instance)
+                prefix = "%s-%s" % (form.prefix, InlineFormSet.get_default_prefix())
+                if request.method == 'POST':
+                    nested_formset = InlineFormSet(request.POST, request.FILES, 
+                                                   instance=form.instance, 
+                                                   prefix=prefix, queryset=nested_inline.queryset(request))
+                else:
+                    nested_formset = InlineFormSet(instance=form.instance, 
+                                                   prefix=prefix, queryset=nested_inline.queryset(request))
+                nested_formsets.append(nested_formset)
+                if nested_inline.inlines:
+                    self.add_nested_inline_formsets(request, nested_inline, nested_formset, depth=depth+1)
+            form.nested_formsets = nested_formsets
+            
+    def wrap_nested_inline_formsets(self, request, inline, formset):
+        media = None
+        def get_media(extra_media):
+            if media:
+                return media + extra_media
+            else:
+                return extra_media
+                        
+        for form in formset.forms:
+            wrapped_nested_formsets = []
+            for nested_inline, nested_formset in zip(inline.get_inline_instances(request), form.nested_formsets):
+                if form.instance.pk:
+                    instance = form.instance
+                else:
+                    instance = None
+                fieldsets = list(nested_inline.get_fieldsets(request))
+                readonly = list(nested_inline.get_readonly_fields(request))
+                prepopulated = dict(nested_inline.get_prepopulated_fields(request))
+                wrapped_nested_formset = helpers.InlineAdminFormSet(nested_inline, nested_formset, 
+                                                             fieldsets, prepopulated, readonly, model_admin=self)
+                wrapped_nested_formsets.append(wrapped_nested_formset)
+                media = get_media(wrapped_nested_formset.media)
+                if nested_inline.inlines:
+                    media = get_media(self.wrap_nested_inline_formsets(request, nested_inline, nested_formset))
+            form.nested_formsets = wrapped_nested_formsets
+        return media
+    
+    def all_valid_with_nesting(self, formsets):
+        "Recursively validate all nested formsets"
+        if not all_valid(formsets):
+            return False
+        for formset in formsets:
+            if not formset.is_bound:
+                pass
+            for form in formset:
+                if hasattr(form, 'nested_formsets'):
+                    if not self.all_valid_with_nesting(form.nested_formsets):
+                        return False
+                    # Here be dragons :(
+                    if not form.cleaned_data:
+                        form._errors["__all__"] = form.error_class([u"Parent object must be created when creating nested inlines."])
+                        return False
+        return True
+            
+
     @csrf_protect_m
     @transaction.commit_on_success
     def add_view(self, request, form_url='', extra_context=None):
@@ -952,7 +1024,9 @@ class ModelAdmin(BaseModelAdmin):
                                   save_as_new="_saveasnew" in request.POST,
                                   prefix=prefix, queryset=inline.queryset(request))
                 formsets.append(formset)
-            if all_valid(formsets) and form_validated:
+                if inline.inlines:
+                    self.add_nested_inline_formsets(request, inline, formset)
+            if self.all_valid_with_nesting(formsets) and form_validated:
                 self.save_model(request, new_object, form, False)
                 self.save_related(request, form, formsets, False)
                 self.log_addition(request, new_object)
@@ -978,6 +1052,8 @@ class ModelAdmin(BaseModelAdmin):
                 formset = FormSet(instance=self.model(), prefix=prefix,
                                   queryset=inline.queryset(request))
                 formsets.append(formset)
+                if inline.inlines:
+                    self.add_nested_inline_formsets(request, inline, formset)
 
         adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
             self.get_prepopulated_fields(request),
@@ -994,6 +1070,8 @@ class ModelAdmin(BaseModelAdmin):
                 fieldsets, prepopulated, readonly, model_admin=self)
             inline_admin_formsets.append(inline_admin_formset)
             media = media + inline_admin_formset.media
+            if inline.inlines:
+                media = media + self.wrap_nested_inline_formsets(request, inline, formset)
 
         context = {
             'title': _('Add %s') % force_text(opts.verbose_name),
@@ -1047,10 +1125,11 @@ class ModelAdmin(BaseModelAdmin):
                 formset = FormSet(request.POST, request.FILES,
                                   instance=new_object, prefix=prefix,
                                   queryset=inline.queryset(request))
-
                 formsets.append(formset)
+                if inline.inlines:
+                    self.add_nested_inline_formsets(request, inline, formset)
 
-            if all_valid(formsets) and form_validated:
+            if self.all_valid_with_nesting(formsets) and form_validated:
                 self.save_model(request, new_object, form, True)
                 self.save_related(request, form, formsets, True)
                 change_message = self.construct_change_message(request, form, formsets)
@@ -1068,6 +1147,8 @@ class ModelAdmin(BaseModelAdmin):
                 formset = FormSet(instance=obj, prefix=prefix,
                                   queryset=inline.queryset(request))
                 formsets.append(formset)
+                if inline.inlines:
+                    self.add_nested_inline_formsets(request, inline, formset)
 
         adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
             self.get_prepopulated_fields(request, obj),
@@ -1084,6 +1165,8 @@ class ModelAdmin(BaseModelAdmin):
                 fieldsets, prepopulated, readonly, model_admin=self)
             inline_admin_formsets.append(inline_admin_formset)
             media = media + inline_admin_formset.media
+            if inline.inlines:
+                media = media + self.wrap_nested_inline_formsets(request, inline, formset)
 
         context = {
             'title': _('Change %s') % force_text(opts.verbose_name),
@@ -1358,6 +1441,7 @@ class InlineModelAdmin(BaseModelAdmin):
     verbose_name = None
     verbose_name_plural = None
     can_delete = True
+    inlines = []
 
     def __init__(self, parent_model, admin_site):
         self.admin_site = admin_site
@@ -1369,6 +1453,20 @@ class InlineModelAdmin(BaseModelAdmin):
         if self.verbose_name_plural is None:
             self.verbose_name_plural = self.model._meta.verbose_name_plural
 
+    def get_inline_instances(self, request):
+        inline_instances = []
+        for inline_class in self.inlines:
+            inline = inline_class(self.model, self.admin_site)
+            if request:
+                if not (inline.has_add_permission(request) or
+                        inline.has_change_permission(request) or
+                        inline.has_delete_permission(request)):
+                    continue
+                if not inline.has_add_permission(request):
+                    inline.max_num = 0
+            inline_instances.append(inline)
+        return inline_instances
+
     @property
     def media(self):
         extra = '' if settings.DEBUG else '.min'
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css
index efec04b..35224aa 100644
--- a/django/contrib/admin/static/admin/css/forms.css
+++ b/django/contrib/admin/static/admin/css/forms.css
@@ -285,7 +285,7 @@ body.popup .submit-row {
     color: #fff;
 }
 
-.inline-group .tabular fieldset.module {
+.inline-group .tabular > fieldset.module {
     border: none;
     border-bottom: 1px solid #ddd;
 }
@@ -358,6 +358,26 @@ body.popup .submit-row {
     outline: 0; /* Remove dotted border around link */
 }
 
+.nested-inline {
+	margin: 10px;
+}
+
+td > .nested-inline {
+	margin: 0px;
+}
+
+.nested-inline-bottom-border {
+	border-bottom: 1px solid #DDDDDD;
+}
+
+.no-bottom-border.row1 > td {
+	border-bottom: solid #EDF3FE 1px;
+}
+
+.no-bottom-border.row2 > td {
+	border-bottom: solid white 1px;
+}
+
 .empty-form {
     display: none;
 }
diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js
index 4dc9459..4d1943d 100644
--- a/django/contrib/admin/static/admin/js/inlines.js
+++ b/django/contrib/admin/static/admin/js/inlines.js
@@ -15,258 +15,444 @@
  * See: http://www.opensource.org/licenses/bsd-license.php
  */
 (function($) {
-  $.fn.formset = function(opts) {
-    var options = $.extend({}, $.fn.formset.defaults, opts);
-    var $this = $(this);
-    var $parent = $this.parent();
-    var updateElementIndex = function(el, prefix, ndx) {
-      var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
-      var replacement = prefix + "-" + ndx;
-      if ($(el).attr("for")) {
-        $(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
-      }
-      if (el.id) {
-        el.id = el.id.replace(id_regex, replacement);
-      }
-      if (el.name) {
-        el.name = el.name.replace(id_regex, replacement);
-      }
-    };
-    var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off");
-    var nextIndex = parseInt(totalForms.val(), 10);
-    var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off");
-    // only show the add button if we are allowed to add more items,
-        // note that max_num = None translates to a blank string.
-    var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0;
-    $this.each(function(i) {
-      $(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
-    });
-    if ($this.length && showAddButton) {
-      var addButton;
-      if ($this.attr("tagName") == "TR") {
-        // If forms are laid out as table rows, insert the
-        // "add" button in a new table row:
-        var numCols = this.eq(-1).children().length;
-        $parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>");
-        addButton = $parent.find("tr:last a");
-      } else {
-        // Otherwise, insert it immediately after the last form:
-        $this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>");
-        addButton = $this.filter(":last").next().find("a");
-      }
-      addButton.click(function(e) {
-        e.preventDefault();
-        var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS");
-        var template = $("#" + options.prefix + "-empty");
-        var row = template.clone(true);
-        row.removeClass(options.emptyCssClass)
-          .addClass(options.formCssClass)
-          .attr("id", options.prefix + "-" + nextIndex);
-        if (row.is("tr")) {
-          // If the forms are laid out in table rows, insert
-          // the remove button into the last table cell:
-          row.children(":last").append('<div><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></div>");
-        } else if (row.is("ul") || row.is("ol")) {
-          // If they're laid out as an ordered/unordered list,
-          // insert an <li> after the last list item:
-          row.append('<li><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></li>");
-        } else {
-          // Otherwise, just insert the remove button as the
-          // last child element of the form's container:
-          row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>");
-        }
-        row.find("*").each(function() {
-          updateElementIndex(this, options.prefix, totalForms.val());
-        });
-        // Insert the new form when it has been fully edited
-        row.insertBefore($(template));
-        // Update number of total forms
-        $(totalForms).val(parseInt(totalForms.val(), 10) + 1);
-        nextIndex += 1;
-        // Hide add button in case we've hit the max, except we want to add infinitely
-        if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) {
-          addButton.parent().hide();
-        }
-        // The delete button of each row triggers a bunch of other things
-        row.find("a." + options.deleteCssClass).click(function(e) {
-          e.preventDefault();
-          // Remove the parent form containing this button:
-          var row = $(this).parents("." + options.formCssClass);
-          row.remove();
-          nextIndex -= 1;
-          // If a post-delete callback was provided, call it with the deleted form:
-          if (options.removed) {
-            options.removed(row);
-          }
-          // Update the TOTAL_FORMS form count.
-          var forms = $("." + options.formCssClass);
-          $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
-          // Show add button again once we drop below max
-          if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) {
-            addButton.parent().show();
-          }
-          // Also, update names and ids for all remaining form controls
-          // so they remain in sequence:
-          for (var i=0, formCount=forms.length; i<formCount; i++)
-          {
-            updateElementIndex($(forms).get(i), options.prefix, i);
-            $(forms.get(i)).find("*").each(function() {
-              updateElementIndex(this, options.prefix, i);
-            });
-          }
-        });
-        // If a post-add callback was supplied, call it with the added form:
-        if (options.added) {
-          options.added(row);
-        }
-      });
-    }
-    return this;
-  };
+	$.fn.formset = function(opts) {
+		var options = $.extend({}, $.fn.formset.defaults, opts);
+		var $this = $(this);
+		var $parent = $this.parent();
+		var updateElementIndex = function(el, prefix, ndx) {
+			var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
+			var replacement = prefix + "-" + ndx;
+			if ($(el).attr("for")) {
+				$(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
+			}
+			if (el.id) {
+				el.id = el.id.replace(id_regex, replacement);
+			}
+			if (el.name) {
+				el.name = el.name.replace(id_regex, replacement);
+			}
+		};
+		var nextIndex = get_no_forms(options.prefix);
 
-  /* Setup plugin defaults */
-  $.fn.formset.defaults = {
-    prefix: "form",          // The form prefix for your django formset
-    addText: "add another",      // Text for the add link
-    deleteText: "remove",      // Text for the delete link
-    addCssClass: "add-row",      // CSS class applied to the add link
-    deleteCssClass: "delete-row",  // CSS class applied to the delete link
-    emptyCssClass: "empty-row",    // CSS class applied to the empty row
-    formCssClass: "dynamic-form",  // CSS class applied to each form in a formset
-    added: null,          // Function called each time a new form is added
-    removed: null          // Function called each time a form is deleted
-  };
+		// Add form classes for dynamic behaviour
+		$this.each(function(i) {
+			$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
+		});
+		// Only show the add button if we are allowed to add more items,
+		// note that max_num = None translates to a blank string.
+		var showAddButton = get_max_forms(options.prefix) === '' || (get_max_forms(options.prefix) - get_no_forms(options.prefix)) > 0;
+		if ($this.length && showAddButton) {
+			var addButton;
+			if ($this.attr("tagName") == "TR") {
+				// If forms are laid out as table rows, insert the
+				// "add" button in a new table row:
+				var numCols = this.eq(-1).children().length;
+				$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>");
+				addButton = $parent.find("tr:last a");
+			} else {
+				// Otherwise, insert it immediately after the last form:
+				$this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>");
+				addButton = $this.filter(":last").next().find("a");
+			}
+			addButton.click(function(e) {
+				e.preventDefault();
+				var nextIndex = get_no_forms(options.prefix);
+				var template = $("#" + options.prefix + "-empty");
+				var row = template.clone(true);
+				row.removeClass(options.emptyCssClass).addClass(options.formCssClass).attr("id", options.prefix + "-" + nextIndex);
+				if (row.is("tr")) {
+					// If the forms are laid out in table rows, insert
+					// the remove button into the last table cell:
+					row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></div>");
+				} else if (row.is("ul") || row.is("ol")) {
+					// If they're laid out as an ordered/unordered list,
+					// insert an <li> after the last list item:
+					row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></li>");
+				} else {
+					// Otherwise, just insert the remove button as the
+					// last child element of the form's container:
+					row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>");
+				}
+				row.find("*").each(function() {
+					updateElementIndex(this, options.prefix, nextIndex);
+				});
+				// when adding something from a cloned formset the id is the same
 
+				// Insert the new form when it has been fully edited
+				row.insertBefore($(template));
 
-  // Tabular inlines ---------------------------------------------------------
-  $.fn.tabularFormset = function(options) {
-    var $rows = $(this);
-    var alternatingRows = function(row) {
-      $($rows.selector).not(".add-row").removeClass("row1 row2")
-        .filter(":even").addClass("row1").end()
-        .filter(":odd").addClass("row2");
-    };
+				// Update number of total forms
+				change_no_forms(options.prefix, true);
 
-    var reinitDateTimeShortCuts = function() {
-      // Reinitialize the calendar and clock widgets by force
-      if (typeof DateTimeShortcuts != "undefined") {
-        $(".datetimeshortcuts").remove();
-        DateTimeShortcuts.init();
-      }
-    };
+				// Hide add button in case we've hit the max, except we want to add infinitely
+				if ((get_max_forms(options.prefix) !== '') && (get_max_forms(options.prefix) - get_no_forms(options.prefix)) <= 0) {
+					addButton.parent().hide();
+				}
 
-    var updateSelectFilter = function() {
-      // If any SelectFilter widgets are a part of the new form,
-      // instantiate a new SelectFilter instance for it.
-      if (typeof SelectFilter != 'undefined'){
-        $('.selectfilter').each(function(index, value){
-          var namearr = value.name.split('-');
-          SelectFilter.init(value.id, namearr[namearr.length-1], false, options.adminStaticPrefix );
-        });
-        $('.selectfilterstacked').each(function(index, value){
-          var namearr = value.name.split('-');
-          SelectFilter.init(value.id, namearr[namearr.length-1], true, options.adminStaticPrefix );
-        });
-      }
-    };
+				// The delete button of each row triggers a bunch of other things
+				row.find("a." + options.deleteCssClass).click(function(e) {
+					e.preventDefault();
+					// Find the row that will be deleted by this button
+					var row = $(this).parents("." + options.formCssClass);
+					// Remove the parent form containing this button:
+					var formset_to_update = row.parent();
+					while (row.next().hasClass('nested-inline-row')) {
+						row.next().remove();
+					}
+					row.remove();
+					change_no_forms(options.prefix, false);
+					// If a post-delete callback was provided, call it with the deleted form:
+					if (options.removed) {
+						options.removed(formset_to_update);
+					}
 
-    var initPrepopulatedFields = function(row) {
-      row.find('.prepopulated_field').each(function() {
-        var field = $(this),
-            input = field.find('input, select, textarea'),
-            dependency_list = input.data('dependency_list') || [],
-            dependencies = [];
-        $.each(dependency_list, function(i, field_name) {
-          dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
-        });
-        if (dependencies.length) {
-          input.prepopulate(dependencies, input.attr('maxlength'));
-        }
-      });
-    };
+				});
 
-    $rows.formset({
-      prefix: options.prefix,
-      addText: options.addText,
-      formCssClass: "dynamic-" + options.prefix,
-      deleteCssClass: "inline-deletelink",
-      deleteText: options.deleteText,
-      emptyCssClass: "empty-form",
-      removed: alternatingRows,
-      added: function(row) {
-        initPrepopulatedFields(row);
-        reinitDateTimeShortCuts();
-        updateSelectFilter();
-        alternatingRows(row);
-      }
-    });
+				if (row.is("tr")) {
+					// If the forms are laid out in table rows, insert
+					// the remove button into the last table cell:
+					// Insert the nested formsets into the new form
+					nested_formsets = create_nested_formset(options.prefix, nextIndex, options, false);
+					if (nested_formsets.length) {
+						row.addClass("no-bottom-border");
+					}
+					nested_formsets.each(function() {
+						if (!$(this).next()) {
+							border_class = "";
+						} else {
+							border_class = " no-bottom-border";
+						}
+						($('<tr class="nested-inline-row' + border_class + '">').html(($('<td>', {
+									colspan : '100%'
+								}).html($(this))))).insertBefore($(template));
+					});
+				} else {
+					// stacked
+					// Insert the nested formsets into the new form
+					nested_formsets = create_nested_formset(options.prefix, nextIndex, options, true);
+					nested_formsets.each(function() {
+						row.append($(this));
+					});
+				}
 
-    return $rows;
-  };
+				// If a post-add callback was supplied, call it with the added form:
+				if (options.added) {
+					options.added(row);
+				}
 
-  // Stacked inlines ---------------------------------------------------------
-  $.fn.stackedFormset = function(options) {
-    var $rows = $(this);
-    var updateInlineLabel = function(row) {
-      $($rows.selector).find(".inline_label").each(function(i) {
-        var count = i + 1;
-        $(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
-      });
-    };
+				nextIndex = nextIndex + 1;
+			});
+		}
+		return this;
+	};
 
-    var reinitDateTimeShortCuts = function() {
-      // Reinitialize the calendar and clock widgets by force, yuck.
-      if (typeof DateTimeShortcuts != "undefined") {
-        $(".datetimeshortcuts").remove();
-        DateTimeShortcuts.init();
-      }
-    };
+	/* Setup plugin defaults */
+	$.fn.formset.defaults = {
+		prefix : "form", // The form prefix for your django formset
+		addText : "add another", // Text for the add link
+		deleteText : "remove", // Text for the delete link
+		addCssClass : "add-row", // CSS class applied to the add link
+		deleteCssClass : "delete-row", // CSS class applied to the delete link
+		emptyCssClass : "empty-row", // CSS class applied to the empty row
+		formCssClass : "dynamic-form", // CSS class applied to each form in a formset
+		added : null, // Function called each time a new form is added
+		removed : null // Function called each time a form is deleted
+	};
 
-    var updateSelectFilter = function() {
-      // If any SelectFilter widgets were added, instantiate a new instance.
-      if (typeof SelectFilter != "undefined"){
-        $(".selectfilter").each(function(index, value){
-          var namearr = value.name.split('-');
-          SelectFilter.init(value.id, namearr[namearr.length-1], false, options.adminStaticPrefix);
-        });
-        $(".selectfilterstacked").each(function(index, value){
-          var namearr = value.name.split('-');
-          SelectFilter.init(value.id, namearr[namearr.length-1], true, options.adminStaticPrefix);
-        });
-      }
-    };
+	// Tabular inlines ---------------------------------------------------------
+	$.fn.tabularFormset = function(options) {
+		var $rows = $(this);
+		var alternatingRows = function(row) {
+			row_number = 0;
+			$($rows.selector).not(".add-row").removeClass("row1 row2").each(function() {
+				$(this).addClass('row' + ((row_number%2)+1));
+				next = $(this).next();
+				while (next.hasClass('nested-inline-row')) {
+					next.addClass('row' + ((row_number%2)+1));
+					next = next.next();
+				}
+				row_number = row_number + 1;
+			});
+		};
 
-    var initPrepopulatedFields = function(row) {
-      row.find('.prepopulated_field').each(function() {
-        var field = $(this),
-            input = field.find('input, select, textarea'),
-            dependency_list = input.data('dependency_list') || [],
-            dependencies = [];
-        $.each(dependency_list, function(i, field_name) {
-          dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id'));
-        });
-        if (dependencies.length) {
-          input.prepopulate(dependencies, input.attr('maxlength'));
-        }
-      });
-    };
+		var reinitDateTimeShortCuts = function() {
+			// Reinitialize the calendar and clock widgets by force
+			if ( typeof DateTimeShortcuts != "undefined") {
+				$(".datetimeshortcuts").remove();
+				DateTimeShortcuts.init();
+			}
+		};
 
-    $rows.formset({
-      prefix: options.prefix,
-      addText: options.addText,
-      formCssClass: "dynamic-" + options.prefix,
-      deleteCssClass: "inline-deletelink",
-      deleteText: options.deleteText,
-      emptyCssClass: "empty-form",
-      removed: updateInlineLabel,
-      added: (function(row) {
-        initPrepopulatedFields(row);
-        reinitDateTimeShortCuts();
-        updateSelectFilter();
-        updateInlineLabel(row);
-      })
-    });
+		var updateSelectFilter = function() {
+			// If any SelectFilter widgets are a part of the new form,
+			// instantiate a new SelectFilter instance for it.
+			if ( typeof SelectFilter != 'undefined') {
+				$('.selectfilter').each(function(index, value) {
+					var namearr = value.name.split('-');
+					SelectFilter.init(value.id, namearr[namearr.length - 1], false, options.adminStaticPrefix);
+				});
+				$('.selectfilterstacked').each(function(index, value) {
+					var namearr = value.name.split('-');
+					SelectFilter.init(value.id, namearr[namearr.length - 1], true, options.adminStaticPrefix);
+				});
+			}
+		};
 
-    return $rows;
-  };
+		var initPrepopulatedFields = function(row) {
+			row.find('.prepopulated_field').each(function() {
+				var field = $(this), input = field.find('input, select, textarea'), dependency_list = input.data('dependency_list') || [], dependencies = [];
+				$.each(dependency_list, function(i, field_name) {
+					dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
+				});
+				if (dependencies.length) {
+					input.prepopulate(dependencies, input.attr('maxlength'));
+				}
+			});
+		};
+
+		$rows.formset({
+			prefix : options.prefix,
+			addText : options.addText,
+			formCssClass : "dynamic-" + options.prefix,
+			deleteCssClass : "inline-deletelink",
+			deleteText : options.deleteText,
+			emptyCssClass : "empty-form",
+			removed : alternatingRows,
+			added : function(row) {
+				initPrepopulatedFields(row);
+				reinitDateTimeShortCuts();
+				updateSelectFilter();
+				alternatingRows(row);
+			}
+		});
+
+		return $rows;
+	};
+
+	// Stacked inlines ---------------------------------------------------------
+	$.fn.stackedFormset = function(options) {
+		var $rows = $(this);
+
+		var update_inline_labels = function(formset_to_update) {
+			formset_to_update.children('.inline-related').not('.empty-form').children('h3').find('.inline_label').each(function(i) {
+				var count = i + 1;
+				$(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
+			});
+		};
+
+		var reinitDateTimeShortCuts = function() {
+			// Reinitialize the calendar and clock widgets by force, yuck.
+			if ( typeof DateTimeShortcuts != "undefined") {
+				$(".datetimeshortcuts").remove();
+				DateTimeShortcuts.init();
+			}
+		};
+
+		var updateSelectFilter = function() {
+			// If any SelectFilter widgets were added, instantiate a new instance.
+			if ( typeof SelectFilter != "undefined") {
+				$(".selectfilter").each(function(index, value) {
+					var namearr = value.name.split('-');
+					SelectFilter.init(value.id, namearr[namearr.length - 1], false, options.adminStaticPrefix);
+				});
+				$(".selectfilterstacked").each(function(index, value) {
+					var namearr = value.name.split('-');
+					SelectFilter.init(value.id, namearr[namearr.length - 1], true, options.adminStaticPrefix);
+				});
+			}
+		};
+
+		var initPrepopulatedFields = function(row) {
+			row.find('.prepopulated_field').each(function() {
+				var field = $(this), input = field.find('input, select, textarea'), dependency_list = input.data('dependency_list') || [], dependencies = [];
+				$.each(dependency_list, function(i, field_name) {
+					dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id'));
+				});
+				if (dependencies.length) {
+					input.prepopulate(dependencies, input.attr('maxlength'));
+				}
+			});
+		};
+
+		$rows.formset({
+			prefix : options.prefix,
+			addText : options.addText,
+			formCssClass : "dynamic-" + options.prefix,
+			deleteCssClass : "inline-deletelink",
+			deleteText : options.deleteText,
+			emptyCssClass : "empty-form",
+			removed : update_inline_labels,
+			added : (function(row) {
+				initPrepopulatedFields(row);
+				reinitDateTimeShortCuts();
+				updateSelectFilter();
+				update_inline_labels(row.parent());
+			})
+		});
+
+		return $rows;
+	};
+
+	function create_nested_formset(parent_formset_prefix, next_form_id, options, add_bottom_border) {
+		var formsets = $(false);
+		// update options
+		// Normalize prefix to something we can rely on
+		var normalized_parent_formset_prefix = parent_formset_prefix.replace(/[-][0-9][-]/g, "-0-");
+		// Check if the form should have nested formsets
+		var nested_inlines = $('#' + normalized_parent_formset_prefix + "-group ." + normalized_parent_formset_prefix + "-nested-inline").not('.cloned');
+		nested_inlines.each(function() {
+			// prefixes for the nested formset
+			var normalized_formset_prefix = $(this).attr('id').split('-group')[0];
+			// = "parent_formset_prefix"-0-"nested_inline_name"_set
+			var formset_prefix = normalized_formset_prefix.replace(normalized_parent_formset_prefix + "-0", parent_formset_prefix + "-" + next_form_id);
+			// = "parent_formset_prefix"-"next_form_id"-"nested_inline_name"_set
+			// Find the normalized formset and clone it
+			var template = $("#" + normalized_formset_prefix + "-group").clone();
+			template.addClass('cloned');
+			if (template.children().first().hasClass('tabular')) {
+				// Template is tabular
+				template.find(".form-row").not(".empty-form").remove();
+				template.find(".nested-inline-row").remove();
+				// Make a new form
+				template_form = template.find("#" + normalized_formset_prefix + "-empty")
+				new_form = template_form.clone().removeClass(options.emptyCssClass).addClass("dynamic-" + formset_prefix);
+				new_form.insertBefore(template_form);
+				// Update Form Properties
+				template.find('#id_' + formset_prefix + '-TOTAL_FORMS').val(1);
+				update_props(template, normalized_formset_prefix, formset_prefix);
+				var add_text = template.find('.add-row').text();
+				template.find('.add-row').remove();
+				template.find('.tabular.inline-related tbody tr.' + formset_prefix + '-not-nested').tabularFormset({
+					prefix : formset_prefix,
+					adminStaticPrefix : options.adminStaticPrefix,
+					addText : add_text,
+					deleteText : options.deleteText
+				});
+				// Create the nested formset
+				var nested_formsets = create_nested_formset(formset_prefix, 0, options, false);
+				if (nested_formsets.length) {
+					template.find(".form-row").addClass('no-bottom-border');
+				}
+				// Insert nested formsets
+				nested_formsets.each(function() {
+					if (!$(this).next()) {
+						border_class = "";
+					} else {
+						border_class = " no-bottom-border";
+					}
+					template.find("#" + formset_prefix + "-empty").before(($('<tr class="nested-inline-row' + border_class + '">').html(($('<td>', {
+								colspan : '100%'
+							}).html($(this))))));
+				});
+			} else {
+				// Template is stacked
+				// Create the nested formset
+				var nested_formsets = create_nested_formset(formset_prefix, 0, options, true);
+				template.find(".inline-related").not(".empty-form").remove();
+				// Make a new form
+				template_form = template.find("#" + normalized_formset_prefix + "-empty")
+				new_form = template_form.clone().removeClass(options.emptyCssClass).addClass("dynamic-" + formset_prefix);
+				new_form.insertBefore(template_form);
+				// Update Form Properties
+				template.find('#id_' + normalized_formset_prefix + '-TOTAL_FORMS').val(1);
+				new_form.find('.inline_label').text('#1');
+				update_props(template, normalized_formset_prefix, formset_prefix);
+				var add_text = template.find('.add-row').text();
+				template.find('.add-row').remove();
+				template.find(".inline-related").stackedFormset({
+					prefix : formset_prefix,
+					adminStaticPrefix : options.adminStaticPrefix,
+					addText : add_text,
+					deleteText : options.deleteText
+				});
+				nested_formsets.each(function() {
+					new_form.append($(this));
+				});
+			}
+			if (add_bottom_border) {
+				template = template.add($('<div class="nested-inline-bottom-border">'));
+			}
+			if (formsets.length) {
+				formsets = formsets.add(template);
+			} else {
+				formsets = template;
+			}
+		});
+		return formsets;
+	};
+
+	function update_props(template, normalized_formset_prefix, formset_prefix) {
+		// Fix template id
+		template.attr('id', template.attr('id').replace(normalized_formset_prefix, formset_prefix));
+		template.find('*').each(function() {
+			if ($(this).attr("for")) {
+				$(this).attr("for", $(this).attr("for").replace(normalized_formset_prefix, formset_prefix));
+			}
+			if ($(this).attr("class")) {
+				$(this).attr("class", $(this).attr("class").replace(normalized_formset_prefix, formset_prefix));
+			}
+			if (this.id) {
+				this.id = this.id.replace(normalized_formset_prefix, formset_prefix);
+			}
+			if (this.name) {
+				this.name = this.name.replace(normalized_formset_prefix, formset_prefix);
+			}
+		});
+		// fix __prefix__ where needed
+		prefix_fix = template.find(".inline-related").first();
+		nextIndex = get_no_forms(formset_prefix);
+		if (prefix_fix.hasClass('tabular')) {
+			// tabular
+			prefix_fix = prefix_fix.find('.form-row').first();
+			prefix_fix.attr('id', prefix_fix.attr('id').replace('-empty', '-' + nextIndex));
+		} else {
+			// stacked
+			prefix_fix.attr('id', prefix_fix.attr('id').replace('-empty', '-' + nextIndex));
+		}
+		prefix_fix.find('*').each(function() {
+			if ($(this).attr("for")) {
+				$(this).attr("for", $(this).attr("for").replace('__prefix__', '0'));
+			}
+			if ($(this).attr("class")) {
+				$(this).attr("class", $(this).attr("class").replace('__prefix__', '0'));
+			}
+			if (this.id) {
+				this.id = this.id.replace('__prefix__', '0');
+			}
+			if (this.name) {
+				this.name = this.name.replace('__prefix__', '0');
+			}
+		});
+	};
+
+	// This returns the amount of forms in the given formset
+	function get_no_forms(formset_prefix) {
+		formset_prop = $("#id_" + formset_prefix + "-TOTAL_FORMS")
+		if (!formset_prop.length) {
+			return 0;
+		}
+		return parseInt(formset_prop.attr("autocomplete", "off").val());
+	}
+
+	function change_no_forms(formset_prefix, increase) {
+		var no_forms = get_no_forms(formset_prefix);
+		if (increase) {
+			$("#id_" + formset_prefix + "-TOTAL_FORMS").attr("autocomplete", "off").val(parseInt(no_forms) + 1);
+		} else {
+			$("#id_" + formset_prefix + "-TOTAL_FORMS").attr("autocomplete", "off").val(parseInt(no_forms) - 1);
+		}
+	};
+
+	// This return the maximum amount of forms in the given formset
+	function get_max_forms(formset_prefix) {
+		var max_forms = $("#id_" + formset_prefix + "-MAX_FORMS").attr("autocomplete", "off").val();
+		if ( typeof max_forms == 'undefined') {
+			return '';
+		}
+		return parseInt(max_forms);
+	};
 })(django.jQuery);
+
+// TODO:
+// Remove border between tabular fieldset and nested inline
+// Fix alternating rows
diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js
index d48ee0a..c2fec35 100644
--- a/django/contrib/admin/static/admin/js/inlines.min.js
+++ b/django/contrib/admin/static/admin/js/inlines.min.js
@@ -1,9 +1,15 @@
-(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),d=c.parent(),i=function(a,e,g){var d=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+g;b(a).attr("for")&&b(a).attr("for",b(a).attr("for").replace(d,e));a.id&&(a.id=a.id.replace(d,e));a.name&&(a.name=a.name.replace(d,e))},f=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off"),g=parseInt(f.val(),10),e=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off"),f=""===e.val()||0<e.val()-f.val();c.each(function(){b(this).not("."+
-a.emptyCssClass).addClass(a.formCssClass)});if(c.length&&f){var h;"TR"==c.attr("tagName")?(c=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+c+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(c.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+
-"-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){i(this,
-a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c<f;c++){i(b(d).get(c),a.prefix,c);b(d.get(c)).find("*").each(function(){i(this,
-a.prefix,c)})}});a.added&&a.added(c)})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};b.fn.tabularFormset=function(d){var a=b(this),c=function(){b(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+
-d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=
-typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),c=function(){b(a.selector).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix,
-addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".form-row .field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),
-DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a}})(django.jQuery);
+(function(b){function j(d,a,e,m){var h=b(!1),n=d.replace(/[-][0-9][-]/g,"-0-");b("#"+n+"-group ."+n+"-nested-inline").not(".cloned").each(function(){var f=b(this).attr("id").split("-group")[0],g=f.replace(n+"-0",d+"-"+a),c=b("#"+f+"-group").clone();c.addClass("cloned");if(c.children().first().hasClass("tabular")){c.find(".form-row").not(".empty-form").remove();c.find(".nested-inline-row").remove();template_form=c.find("#"+f+"-empty");new_form=template_form.clone().removeClass(e.emptyCssClass).addClass("dynamic-"+
+g);new_form.insertBefore(template_form);c.find("#id_"+g+"-TOTAL_FORMS").val(1);p(c,f,g);f=c.find(".add-row").text();c.find(".add-row").remove();c.find(".tabular.inline-related tbody tr."+g+"-not-nested").tabularFormset({prefix:g,adminStaticPrefix:e.adminStaticPrefix,addText:f,deleteText:e.deleteText});var k=j(g,0,e,!1);k.length&&c.find(".form-row").addClass("no-bottom-border");k.each(function(){border_class=b(this).next()?" no-bottom-border":"";c.find("#"+g+"-empty").before(b('<tr class="nested-inline-row'+
+border_class+'">').html(b("<td>",{colspan:"100%"}).html(b(this))))})}else k=j(g,0,e,!0),c.find(".inline-related").not(".empty-form").remove(),template_form=c.find("#"+f+"-empty"),new_form=template_form.clone().removeClass(e.emptyCssClass).addClass("dynamic-"+g),new_form.insertBefore(template_form),c.find("#id_"+f+"-TOTAL_FORMS").val(1),new_form.find(".inline_label").text("#1"),p(c,f,g),f=c.find(".add-row").text(),c.find(".add-row").remove(),c.find(".inline-related").stackedFormset({prefix:g,adminStaticPrefix:e.adminStaticPrefix,
+addText:f,deleteText:e.deleteText}),k.each(function(){new_form.append(b(this))});m&&(c=c.add(b('<div class="nested-inline-bottom-border">')));h=h.length?h.add(c):c});return h}function p(d,a,e){d.attr("id",d.attr("id").replace(a,e));d.find("*").each(function(){b(this).attr("for")&&b(this).attr("for",b(this).attr("for").replace(a,e));b(this).attr("class")&&b(this).attr("class",b(this).attr("class").replace(a,e));this.id&&(this.id=this.id.replace(a,e));this.name&&(this.name=this.name.replace(a,e))});
+prefix_fix=d.find(".inline-related").first();nextIndex=i(e);prefix_fix.hasClass("tabular")&&(prefix_fix=prefix_fix.find(".form-row").first());prefix_fix.attr("id",prefix_fix.attr("id").replace("-empty","-"+nextIndex));prefix_fix.find("*").each(function(){b(this).attr("for")&&b(this).attr("for",b(this).attr("for").replace("__prefix__","0"));b(this).attr("class")&&b(this).attr("class",b(this).attr("class").replace("__prefix__","0"));this.id&&(this.id=this.id.replace("__prefix__","0"));this.name&&(this.name=
+this.name.replace("__prefix__","0"))})}function i(d){formset_prop=b("#id_"+d+"-TOTAL_FORMS");return!formset_prop.length?0:parseInt(formset_prop.attr("autocomplete","off").val())}function q(d,a){var e=i(d);a?b("#id_"+d+"-TOTAL_FORMS").attr("autocomplete","off").val(parseInt(e)+1):b("#id_"+d+"-TOTAL_FORMS").attr("autocomplete","off").val(parseInt(e)-1)}function l(d){d=b("#id_"+d+"-MAX_FORMS").attr("autocomplete","off").val();return"undefined"==typeof d?"":parseInt(d)}b.fn.formset=function(d){var a=
+b.extend({},b.fn.formset.defaults,d),e=b(this),d=e.parent();i(a.prefix);e.each(function(){b(this).not("."+a.emptyCssClass).addClass(a.formCssClass)});var m=""===l(a.prefix)||0<l(a.prefix)-i(a.prefix);if(e.length&&m){var h;"TR"==e.attr("tagName")?(e=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+e+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(e.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+
+a.addText+"</a></div>"),h=e.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=i(a.prefix),e=b("#"+a.prefix+"-empty"),c=e.clone(!0);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+f);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+
+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){var c=a.prefix,d=RegExp("("+c+"-(\\d+|__prefix__))"),c=c+"-"+f;b(this).attr("for")&&b(this).attr("for",b(this).attr("for").replace(d,c));this.id&&(this.id=this.id.replace(d,c));this.name&&(this.name=this.name.replace(d,c))});c.insertBefore(b(e));q(a.prefix,!0);""!==l(a.prefix)&&0>=l(a.prefix)-i(a.prefix)&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(c){c.preventDefault();for(var c=
+b(this).parents("."+a.formCssClass),d=c.parent();c.next().hasClass("nested-inline-row");)c.next().remove();c.remove();q(a.prefix,!1);a.removed&&a.removed(d)});c.is("tr")?(nested_formsets=j(a.prefix,f,a,!1),nested_formsets.length&&c.addClass("no-bottom-border"),nested_formsets.each(function(){border_class=b(this).next()?" no-bottom-border":"";b('<tr class="nested-inline-row'+border_class+'">').html(b("<td>",{colspan:"100%"}).html(b(this))).insertBefore(b(e))})):(nested_formsets=j(a.prefix,f,a,!0),
+nested_formsets.each(function(){c.append(b(this))}));a.added&&a.added(c);f+=1})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};b.fn.tabularFormset=function(d){var a=b(this),e=function(){row_number=0;b(a.selector).not(".add-row").removeClass("row1 row2").each(function(){b(this).addClass("row"+(row_number%2+1));for(next=b(this).next();next.hasClass("nested-inline-row");)next.addClass("row"+
+(row_number%2+1)),next=next.next();row_number+=1})};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:e,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),e=d.data("dependency_list")||[],f=[];b.each(e,function(b,c){f.push("#"+a.find(".field-"+c).find("input, select, textarea").attr("id"))});f.length&&d.prepopulate(f,d.attr("maxlength"))});
+"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(b,a){var e=a.name.split("-");SelectFilter.init(a.id,e[e.length-1],!1,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(b,a){var e=a.name.split("-");SelectFilter.init(a.id,e[e.length-1],!0,d.adminStaticPrefix)}));e(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),e=function(a){a.children(".inline-related").not(".empty-form").children("h3").find(".inline_label").each(function(a){a+=
+1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:e,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),e=d.data("dependency_list")||[],f=[];b.each(e,function(b,c){f.push("#"+a.find(".form-row .field-"+c).find("input, select, textarea").attr("id"))});f.length&&d.prepopulate(f,
+d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var e=b.name.split("-");SelectFilter.init(b.id,e[e.length-1],!1,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var e=b.name.split("-");SelectFilter.init(b.id,e[e.length-1],!0,d.adminStaticPrefix)}));e(a.parent())}});return a}})(django.jQuery);
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 2025dd8..c0c5389 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -1,13 +1,14 @@
 {% load i18n admin_static %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
-  <h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2>
-{{ inline_admin_formset.formset.management_form }}
-{{ inline_admin_formset.formset.non_form_errors }}
+<div class="inline-group{% if recursive_formset %} {{ recursive_formset.formset.prefix|default:"Root" }}-nested-inline nested-inline{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group">
+{% with recursive_formset=inline_admin_formset stacked_template='admin/edit_inline/stacked.html' tabular_template='admin/edit_inline/tabular.html'%}
+  <h2>{{ recursive_formset.opts.verbose_name_plural|title }}</h2>
+{{ recursive_formset.formset.management_form }}
+{{ recursive_formset.formset.non_form_errors }}
 
-{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
-  <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>
+{% for inline_admin_form in recursive_formset %}<div class="inline-related{% if forloop.last %} empty-form last-related{% endif %}" id="{{ recursive_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+  <h3><b>{{ recursive_formset.opts.verbose_name|title }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>
     {% if inline_admin_form.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% trans "View on site" %}</a>{% endif %}
-    {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
+    {% if recursive_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
   </h3>
   {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
   {% for fieldset in inline_admin_form %}
@@ -15,16 +16,27 @@
   {% endfor %}
   {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
   {{ inline_admin_form.fk_field.field }}
+  {% if inline_admin_form.form.nested_formsets %}
+    {% for inline_admin_formset in inline_admin_form.form.nested_formsets %}
+   	  {% if inline_admin_formset.opts.template == stacked_template %}
+        {% include stacked_template %}
+   	  {% else %}
+   	 	{% include tabular_template %}
+   	  {% endif %}
+   	  <div class="nested-inline-bottom-border"></div>
+    {% endfor %}
+  {% endif %}
 </div>{% endfor %}
 </div>
 
 <script type="text/javascript">
 (function($) {
-  $("#{{ inline_admin_formset.formset.prefix }}-group .inline-related").stackedFormset({
-    prefix: '{{ inline_admin_formset.formset.prefix }}',
+  $("#{{ recursive_formset.formset.prefix }}-group > .inline-related").stackedFormset({
+    prefix: '{{ recursive_formset.formset.prefix }}',
     adminStaticPrefix: '{% static "admin/" %}',
-    deleteText: "{% trans "Remove" %}",
-    addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}"
+    addText: "{% blocktrans with verbose_name=recursive_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}",
+    deleteText: "{% trans "Remove" %}"
   });
 })(django.jQuery);
 </script>
+{% endwith %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index f2757ed..1b0de0e 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -1,29 +1,30 @@
 {% load i18n admin_static admin_modify %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
+<div class="inline-group{% if recursive_formset %} {{ recursive_formset.formset.prefix|default:"Root" }}-nested-inline nested-inline{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group">
+{% with recursive_formset=inline_admin_formset stacked_template='admin/edit_inline/stacked.html' tabular_template='admin/edit_inline/tabular.html'%}
   <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
-{{ inline_admin_formset.formset.management_form }}
+{{ recursive_formset.formset.management_form }}
 <fieldset class="module">
-   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
-   {{ inline_admin_formset.formset.non_form_errors }}
+   <h2>{{ recursive_formset.opts.verbose_name_plural|capfirst }}</h2>
+   {{ recursive_formset.formset.non_form_errors }}
    <table>
      <thead><tr>
-     {% for field in inline_admin_formset.fields %}
+     {% for field in recursive_formset.fields %}
        {% if not field.widget.is_hidden %}
          <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}
          {% if field.help_text %}&nbsp;<img src="{% static "admin/img/icon-unknown.gif" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %}
          </th>
        {% endif %}
      {% endfor %}
-     {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
+     {% if recursive_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
      </tr></thead>
 
      <tbody>
-     {% for inline_admin_form in inline_admin_formset %}
+     {% for inline_admin_form in recursive_formset %}
         {% if inline_admin_form.form.non_field_errors %}
         <tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
         {% endif %}
-        <tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
-             id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+        <tr class="form-row {% cycle "row1" "row2" as row_number_class %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %} {{ recursive_formset.formset.prefix }}-not-nested {% if inline_admin_form.form.nested_formsets %} no-bottom-border {% endif %}"
+             id="{{ recursive_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
         <td class="original">
           {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
           {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
@@ -55,10 +56,23 @@
             {% endfor %}
           {% endfor %}
         {% endfor %}
-        {% if inline_admin_formset.formset.can_delete %}
+        {% if recursive_formset.formset.can_delete %}
           <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
         {% endif %}
         </tr>
+        {% if inline_admin_form.form.nested_formsets %}
+   	 	  {% for inline_admin_formset in inline_admin_form.form.nested_formsets %}
+   	 	  <tr class="nested-inline-row {{ row_number_class }}{% if not forloop.last %} no-bottom-border{% endif %}">
+   	 	    <td colspan="0">
+	   	 	  {% if inline_admin_formset.opts.template == stacked_template %}
+	   	 	    {% include stacked_template with indent=0 prev_prefix=recursive_formset.formset.prefix %}
+	   	 	  {% else %}
+	   	 		{% include tabular_template with indent=0 prev_prefix=recursive_formset.formset.prefix %}
+	   	 	  {% endif %}
+   	 		</td>
+   	 	  </tr>
+     	  {% endfor %}
+   		{% endif %}
      {% endfor %}
      </tbody>
    </table>
@@ -67,13 +81,13 @@
 </div>
 
 <script type="text/javascript">
-
 (function($) {
-  $("#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr").tabularFormset({
-    prefix: "{{ inline_admin_formset.formset.prefix }}",
+  $("#{{ recursive_formset.formset.prefix }}-group .tabular.inline-related tbody tr.{{ recursive_formset.formset.prefix }}-not-nested").tabularFormset({
+    prefix: "{{ recursive_formset.formset.prefix }}",
     adminStaticPrefix: '{% static "admin/" %}',
-    addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
+    addText: "{% blocktrans with recursive_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
     deleteText: "{% trans 'Remove' %}"
   });
 })(django.jQuery);
 </script>
+{% endwith %}
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
index 7c62c1a..6fe6216 100644
--- a/django/contrib/admin/tests.py
+++ b/django/contrib/admin/tests.py
@@ -2,6 +2,7 @@ from django.test import LiveServerTestCase
 from django.utils.importlib import import_module
 from django.utils.unittest import SkipTest
 from django.utils.translation import ugettext as _
+from selenium import webdriver
 
 class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
     webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
@@ -13,6 +14,7 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
             module, attr = cls.webdriver_class.rsplit('.', 1)
             mod = import_module(module)
             WebDriver = getattr(mod, attr)
+            #Avoid startup screen
             cls.selenium = WebDriver()
         except Exception as e:
             raise SkipTest('Selenium webdriver "%s" not installed or not '
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 06751df..b610616 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -1369,7 +1369,27 @@ information.
 
     The difference between these two is merely the template used to render
     them.
+    
+    .. versionadded:: 1.5
+    
+    You can also display inlines inside an inline. Suppose you have a third model::
+	
+         class Chapter(models.Model):
+            name = models.CharField(max_length=100)
+            book = models.ForeignKey(Book)
+
+    These inlines work exactly the same as normal inlines, but they are specified 
+    inside a ``InlineModelAdmin.inlines``::
 
+        class BookInline(admin.TabularInline):
+           model = Book
+           inlines = [
+            	ChapterInline,
+           ]
+
+        class ChapterInline(admin.StackedInline):
+    	   model = Chapter
+	
 ``InlineModelAdmin`` options
 -----------------------------
 
@@ -1399,6 +1419,10 @@ adds some of its own (the shared features are actually defined in the
 - :meth:`~ModelAdmin.has_change_permission`
 - :meth:`~ModelAdmin.has_delete_permission`
 
+.. versionadded:: 1.5
+
+- :meth:`~ModelAdmin.inlines`
+
 The ``InlineModelAdmin`` class adds:
 
 .. attribute:: InlineModelAdmin.model
diff --git a/tests/regressiontests/admin_inlines/admin.py b/tests/regressiontests/admin_inlines/admin.py
index cf51fa4..3f2d067 100644
--- a/tests/regressiontests/admin_inlines/admin.py
+++ b/tests/regressiontests/admin_inlines/admin.py
@@ -123,6 +123,34 @@ class ChildModel1Inline(admin.TabularInline):
 
 class ChildModel2Inline(admin.StackedInline):
     model = ChildModel2
+    
+class FurnitureInline(admin.StackedInline):
+    model = Furniture
+    extra = 1
+    
+class InhabitantInline(admin.StackedInline):
+    model = Inhabitant
+    extra = 1
+    inlines = [ FurnitureInline, ]
+    
+class AppartementInline(admin.TabularInline):
+    model = Appartement
+    extra = 1
+    inlines = [ InhabitantInline, ]
+    
+class MonumentInline(admin.StackedInline):
+    model = Monument
+    extra = 1
+    
+class BuildingInline(admin.TabularInline):
+    model = Building
+    extra = 1
+    inlines = [ AppartementInline, ]
+    
+class CityInline(admin.StackedInline):
+    model = City
+    extra = 1
+    inlines = [BuildingInline, MonumentInline, ]
 
 
 site.register(TitleCollection, inlines=[TitleInline])
@@ -141,4 +169,11 @@ site.register(Holder4, Holder4Admin)
 site.register(Author, AuthorAdmin)
 site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
 site.register(ProfileCollection, inlines=[ProfileInline])
-site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline])
\ No newline at end of file
+site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline])
+site.register(Country, inlines=[CityInline])
+site.register(City)
+site.register(Building)
+site.register(Monument)
+site.register(Appartement)
+site.register(Inhabitant)
+site.register(Furniture)
\ No newline at end of file
diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py
index b004d5f..c9c522c 100644
--- a/tests/regressiontests/admin_inlines/models.py
+++ b/tests/regressiontests/admin_inlines/models.py
@@ -180,3 +180,52 @@ class Profile(models.Model):
     collection = models.ForeignKey(ProfileCollection, blank=True, null=True)
     first_name = models.CharField(max_length=100)
     last_name = models.CharField(max_length=100)
+    
+class Country(models.Model):
+    name = models.CharField(max_length=100)
+    
+    def __unicode__(self):
+        return self.name
+
+class City(models.Model):
+    name = models.CharField(max_length=100)
+    population = models.IntegerField()
+    country = models.ForeignKey(Country)
+    
+    def __unicode__(self):
+        return self.name
+
+class Building(models.Model):
+    name = models.CharField(max_length=100)
+    city = models.ForeignKey(City)
+    
+    def __unicode__(self):
+        return self.name
+
+class Appartement(models.Model):
+    name = models.CharField(max_length=100)
+    building = models.ForeignKey(Building)
+    
+    def __unicode__(self):
+        return self.name
+
+class Inhabitant(models.Model):
+    name = models.CharField(max_length=100)
+    appartement = models.ForeignKey(Appartement)
+    
+    def __unicode__(self):
+        return self.name
+
+class Furniture(models.Model):
+    name = models.CharField(max_length=100)
+    inhabitant = models.ForeignKey(Inhabitant)
+    
+    def __unicode__(self):
+        return self.name
+    
+class Monument(models.Model):
+    name = models.CharField(max_length=100)
+    city = models.ForeignKey(City)
+    
+    def __unicode__(self):
+        return self.name
\ No newline at end of file
diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
index 5bb6077..bc9c5d3 100644
--- a/tests/regressiontests/admin_inlines/tests.py
+++ b/tests/regressiontests/admin_inlines/tests.py
@@ -8,11 +8,11 @@ from django.test import TestCase
 from django.test.utils import override_settings
 
 # local test models
-from .admin import InnerInline, TitleInline, site
+from .admin import InnerInline
 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
     OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
-    ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
-    Title)
+    ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, 
+    Country, City, Building, Appartement, Inhabitant, Furniture, Monument)
 
 
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -560,15 +560,19 @@ class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
             'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a').click()
         self.selenium.find_element_by_css_selector(
             'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a').click()
-        # Verify that they're gone and that the IDs have been re-sequenced
+        # Verify that they're gone
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
             '#profile_set-group table tr.dynamic-profile_set')), 3)
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
             'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
-            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 0)
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
-            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 0)
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-3')), 1)
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-4')), 1)
 
     def test_alternating_rows(self):
         self.admin_login(username='super', password='secret')
@@ -583,9 +587,193 @@ class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
             "%s.row1" % row_selector)), 2, msg="Expect two row1 styled rows")
         self.assertEqual(len(self.selenium.find_elements_by_css_selector(
-            "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row")
+            "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row")    
 
+    def test_add_nested_inlines(self):
+        self.admin_login(username='super', password='secret')
+        self.selenium.get('%s%s' % (self.live_server_url,
+            '/admin/admin_inlines/country/add/'))
+        
+        # Add some cities
+        self.selenium.find_element_by_link_text('Add another City').click()
+        self.selenium.find_element_by_link_text('Add another City').click()
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-1 .nested-inline-row .nested-inline-row #city_set-1-building_set-0-appartement_set-0-inhabitant_set-0 " +
+            "#city_set-1-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0")), 1, "Expected furniture set in second city");
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-2 .nested-inline-row #city_set-2-building_set-0-appartement_set-0-inhabitant_set-0 " +
+            "#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0")), 1, "Expected furniture set in third city");
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-1 #city_set-1-monument_set-0")), 1, "Expected monument set in second city");
+        # Add monument in first city
+        self.selenium.find_elements_by_css_selector('#city_set-0-monument_set-group > .add-row a')[0].click()
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-0 #city_set-0-monument_set-1")), 1, "Expected second monument in first city")
+        # Add building in second city
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-0 ~ .add-row a')[0].click()
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-1 #city_set-1-building_set-1")), 1, "Expected second building in second city");
+        # Add apartement in second building of second city
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0 ~ .add-row a')[0].click()
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-1 .nested-inline-row #city_set-1-building_set-1-appartement_set-1")), 1, "Expected second appartement in second building of second city");
+        # Add inhabitants in third city
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click()
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-2 .nested-inline-row .nested-inline-row #city_set-2-building_set-0-appartement_set-0-inhabitant_set-1")), 1, "Expected second inhabitant in third city");
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-2 .nested-inline-row .nested-inline-row #city_set-2-building_set-0-appartement_set-0-inhabitant_set-2")), 1, "Expected third inhabitant in third city");
+        # Add furniture in first city
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(
+            "#city_set-0 .nested-inline-row .nested-inline-row #city_set-0-building_set-0-appartement_set-0-inhabitant_set-0 "+
+            "#city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-1")), 1, "Expected second furniture in first city");        
+    
+    def test_delete_nested_inlines(self):
+        self.admin_login(username='super', password='secret')
+        self.selenium.get('%s%s' % (self.live_server_url,
+            '/admin/admin_inlines/country/add/'))
+        
+        # Add 2 cities
+        self.selenium.find_element_by_link_text('Add another City').click()
+        self.selenium.find_element_by_link_text('Add another City').click()
+        # Delete second city
+        self.selenium.find_elements_by_css_selector('#city_set-1 > h3 a.inline-deletelink')[0].click()
+        # Check if only two cities
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set")), 2, "Expected 2 cities")
+        # Add 2 appartements in first city
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0 ~ .add-row a')[0].click()
+        # Delete second appartement
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-1 > td.delete a')[0].click()
+        # Check if only two appartements in first city
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set-0-building_set-0-appartement_set")), 2, "Expected 2 Inhabitants")
+        # Check that nested inlines have also been deleted
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set-0-building_set-0-appartement_set")), 2, "Expected 2 Inhabitants")
+        # Add 4 furniture in second city
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        # Delete second and fourth
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-1 > h3 a.inline-deletelink')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-3 > h3 a.inline-deletelink')[0].click()
+        # Check if only 3 furniture
+        self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set")), 3, "Expected 3 furniture")
+    
+    def test_save_nested_inlines(self):
+        self.admin_login(username='super', password='secret')
+        self.selenium.get('%s%s' % (self.live_server_url,
+            '/admin/admin_inlines/country/add/'))
+        
+        # Add City
+        self.selenium.find_element_by_link_text('Add another City').click()
+        # Add Buildings
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-0 ~ .add-row a')[0].click()
+        # Add Appartements
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0 ~ .add-row a')[0].click()
+        # Add Inhabitant
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click()
+        # Add Furniture
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click()
+        # Add Monument
+        self.selenium.find_elements_by_css_selector('#city_set-0-monument_set-0 ~ .add-row a')[0].click()
+        self.selenium.find_elements_by_css_selector('#city_set-1-monument_set-0 ~ .add-row a')[0].click()
+        # Input Data
+        self.selenium.find_element_by_css_selector('#id_name').send_keys('Belgium')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-name').send_keys('C 1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-population').send_keys('10')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-name').send_keys('B 1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-name').send_keys('A 1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-name').send_keys('I 1.1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 1.1.1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-1-name').send_keys('I 1.1.1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-1-furniture_set-0-name').send_keys('F 1.1.1.2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-name').send_keys('A 1.1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-name').send_keys('I 1.1.2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-0-name').send_keys('F 1.1.2.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-1-name').send_keys('F 1.1.2.1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-name').send_keys('B 1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-appartement_set-0-name').send_keys('A 1.2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-appartement_set-0-inhabitant_set-0-name').send_keys('I 1.2.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 1.2.1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-monument_set-0-name').send_keys('M 1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-0-monument_set-1-name').send_keys('M 1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-name').send_keys('C 2')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-population').send_keys('10')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-name').send_keys('B 2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-appartement_set-0-name').send_keys('A 2.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-appartement_set-0-inhabitant_set-0-name').send_keys('I 2.1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 2.1.1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-name').send_keys('B 2.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-name').send_keys('A 2.2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-0-name').send_keys('I 2.2.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 2.2.1.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-1-name').send_keys('I 2.2.1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-1-furniture_set-0-name').send_keys('F 2.2.1.2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-name').send_keys('A 2.2.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-name').send_keys('I 2.2.2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-furniture_set-0-name').send_keys('F 2.2.2.1.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-furniture_set-1-name').send_keys('F 2.2.2.1.2')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-monument_set-0-name').send_keys('M 2.1')
+        self.selenium.find_element_by_css_selector('#id_city_set-1-monument_set-1-name').send_keys('M 2.2')
+        # Delete inhabitant 2.2.1.2
+        self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0-inhabitant_set-1 > h3 a.inline-deletelink')[0].click()
+        # Delete furniture 1.1.2.1.2
+        self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-1 > h3 a.inline-deletelink')[0].click()
+        # Save
+        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
+        
+        try:
+            # Wait for the next page to be loaded.
+            self.wait_loaded_tag('body')
+        except TimeoutException:
+            # IE7 occasionnally returns an error "Internet Explorer cannot
+            # display the webpage" and doesn't load the next page. We just
+            # ignore it.
+            pass
 
+        # Check if saved correctly
+        self.assertEqual(Country.objects.all().count(), 1)
+        self.assertEqual(City.objects.get(name="C 1").country, Country.objects.get(name="Belgium"))
+        self.assertEqual(Building.objects.get(name="B 1.1").city, City.objects.get(name="C 1"))
+        self.assertEqual(Building.objects.get(name="B 1.2").city, City.objects.get(name="C 1"))
+        self.assertEqual(Building.objects.get(name="B 2.1").city, City.objects.get(name="C 2"))
+        self.assertEqual(Building.objects.get(name="B 2.2").city, City.objects.get(name="C 2"))
+        self.assertEqual(Appartement.objects.get(name="A 1.1.1").building, Building.objects.get(name="B 1.1"))
+        self.assertEqual(Appartement.objects.get(name="A 1.1.2").building, Building.objects.get(name="B 1.1"))
+        self.assertEqual(Appartement.objects.get(name="A 1.2.1").building, Building.objects.get(name="B 1.2"))
+        self.assertEqual(Appartement.objects.get(name="A 2.1.1").building, Building.objects.get(name="B 2.1"))
+        self.assertEqual(Appartement.objects.get(name="A 2.2.1").building, Building.objects.get(name="B 2.2"))
+        self.assertEqual(Appartement.objects.get(name="A 2.2.2").building, Building.objects.get(name="B 2.2"))
+        self.assertEqual(Inhabitant.objects.get(name="I 1.1.1.1").appartement, Appartement.objects.get(name="A 1.1.1"))
+        self.assertEqual(Inhabitant.objects.get(name="I 1.1.1.2").appartement, Appartement.objects.get(name="A 1.1.1"))
+        self.assertEqual(Inhabitant.objects.get(name="I 1.1.2.1").appartement, Appartement.objects.get(name="A 1.1.2"))
+        self.assertEqual(Inhabitant.objects.get(name="I 1.2.1.1").appartement, Appartement.objects.get(name="A 1.2.1"))
+        self.assertEqual(Inhabitant.objects.get(name="I 2.1.1.1").appartement, Appartement.objects.get(name="A 2.1.1"))
+        self.assertEqual(Inhabitant.objects.get(name="I 2.2.1.1").appartement, Appartement.objects.get(name="A 2.2.1"))
+        self.assertEqual(len(Inhabitant.objects.filter(name="I 2.2.1.2")), 0)
+        self.assertEqual(Inhabitant.objects.get(name="I 2.2.2.1").appartement, Appartement.objects.get(name="A 2.2.2"))
+        self.assertEqual(Furniture.objects.get(name="F 1.1.1.1.1").inhabitant, Inhabitant.objects.get(name="I 1.1.1.1"))
+        self.assertEqual(Furniture.objects.get(name="F 1.1.1.2.1").inhabitant, Inhabitant.objects.get(name="I 1.1.1.2"))
+        self.assertEqual(Furniture.objects.get(name="F 1.1.2.1.1").inhabitant, Inhabitant.objects.get(name="I 1.1.2.1"))
+        self.assertEqual(len(Furniture.objects.filter(name="F 1.1.2.1.2")), 0)
+        self.assertEqual(Furniture.objects.get(name="F 1.2.1.1.1").inhabitant, Inhabitant.objects.get(name="I 1.2.1.1"))
+        self.assertEqual(Furniture.objects.get(name="F 2.1.1.1.1").inhabitant, Inhabitant.objects.get(name="I 2.1.1.1"))
+        self.assertEqual(Furniture.objects.get(name="F 2.2.1.1.1").inhabitant, Inhabitant.objects.get(name="I 2.2.1.1"))
+        self.assertEqual(len(Furniture.objects.filter(name="F 2.2.1.2.1")), 0)
+        self.assertEqual(Furniture.objects.get(name="F 2.2.2.1.1").inhabitant, Inhabitant.objects.get(name="I 2.2.2.1"))
+        self.assertEqual(Furniture.objects.get(name="F 2.2.2.1.2").inhabitant, Inhabitant.objects.get(name="I 2.2.2.1"))
+        self.assertEqual(Monument.objects.get(name="M 1.1").city, City.objects.get(name="C 1"))
+        self.assertEqual(Monument.objects.get(name="M 1.2").city, City.objects.get(name="C 1"))
+        self.assertEqual(Monument.objects.get(name="M 2.1").city, City.objects.get(name="C 2"))
+        self.assertEqual(Monument.objects.get(name="M 2.2").city, City.objects.get(name="C 2"))
+    
 class SeleniumChromeTests(SeleniumFirefoxTests):
     webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
 
