Index: django/contrib/admin/media/css/global.css
===================================================================
--- django/contrib/admin/media/css/global.css	(revision 7997)
+++ django/contrib/admin/media/css/global.css	(working copy)
@@ -139,3 +139,7 @@
 /* OBJECT HISTORY */
 table#change-history { width:100%; }
 table#change-history tbody th { width:16em; }
+
+/* INLINE DELETING */
+td.delete { width: 20px; }
+#ajax-indicator { position: absolute; top: 0px; right: 0px; padding: 5px 10px; border: 1px solid #ddd; background: #ffc; }
Index: django/contrib/admin/media/js/admin/InlineObjectsTabular.js
===================================================================
--- django/contrib/admin/media/js/admin/InlineObjectsTabular.js	(revision 0)
+++ django/contrib/admin/media/js/admin/InlineObjectsTabular.js	(revision 0)
@@ -0,0 +1,226 @@
+(function() {
+
+    if (! gettext) gettext = function(x) { return x}
+
+    String.prototype.trim = function() {
+        return this.replace(/^\s+|\s+$/g, '');
+    }
+
+    Array.prototype.filter = function(fn) {
+        var results = [];
+		for (var i = 0, l = this.length; i < l; i++){
+			if (fn(this[i])) results.push(this[i]);
+		}
+		return results;
+    }
+
+    Array.prototype.map = function(fn) {
+        var results = [];
+		for (var i = 0, l = this.length; i < l; i++){
+			results.push(fn(this[i]));
+		}
+		return results;
+    }
+
+	Array.prototype.flatten = function(){
+		var array = [];
+		for (var i = 0, l = this.length; i < l; i++){
+			array = array.concat(this[i]);
+		}
+		return array;
+	}
+
+    function $A(e) {
+        var r = [];
+        for(var i=0, l=e.length; i<l; i++) r.push(e[i]);
+        return r;
+    }
+
+    function _prepare_selector(selector, point) {
+        var queue = selector.split(/\s/).map(function(x){return x.trim()});
+        var points = [point];
+        for(var i=0, l=queue.length; i<l; i++) {
+            var new_points = [];
+            for(var j=0, k=points.length; j<k; j++) {
+                var tmp = _select(queue[i], points[j]);
+                if (!tmp) break;
+                for(var p=0, o=tmp.length; p<o; p++) {
+                    new_points.push(tmp[p]);
+                }
+            }
+            points = new_points;
+        }
+        return points;
+    }
+    
+    function _select(selector, point) {
+        var chunks = selector.split(/[\.]/);
+        var splitters = selector.match(/[\.]/g);
+        if (splitters) {
+            if(chunks.length > splitters.length){
+                splitters.unshift(null);
+            }
+            var f_ch = chunks.shift(), f_sel = splitters.shift();
+            if (f_sel == '.') {
+                els=point.getElementsByClassName(f_ch);
+            } else if (f_sel == null) {
+                els = point.getElementsByTagName(f_ch);
+            }
+            els = $A(els);
+            for(var i=0, l=chunks.length; i<l; i++){
+                var ch = chunks[i], sel = splitters[i], tmp = [];
+                
+                for(var j=0, k=els.length; j<k; j++){
+                    if(els[j].className.match(ch)) tmp.push(els[j]);
+                }
+                els = tmp;
+            }
+            return $A(els);
+            
+        } else {
+            return $A(point.getElementsByTagName(chunks.shift()));
+        }
+    }
+    
+    function show_loading() {
+        hide_loading();
+        var l = document.createElement('div');
+        l.appendChild(document.createTextNode(gettext('loading ...')));
+        l.id = 'ajax-indicator';
+        document.body.appendChild(l);
+    }
+    
+    function hide_loading() {
+        var l = $('ajax-indicator');
+        if(l) l.parentNode.removeChild(l);
+    }
+    
+    function $(e) {
+        return document.getElementById(e);
+    }
+    
+    function $$(e, point) {
+        point = point ? point : document;
+        return e.split(',')
+            .map(function(x){return x.trim()})
+            .filter(function(x){return !!x})
+            .map(function(x){return _prepare_selector(x, point)})
+            .flatten();
+    }
+
+    function make_delete_handle(h) {
+        var p = h.parentNode;
+        p.removeChild(h);
+        var origin = $$('td.original input', p.parentNode)[0];
+        if (! origin) return;
+        var id = origin.value, name = origin.name.match(/^(\w+)-/i)[1];
+        var d_handle = document.createElement('span');
+        d_handle.className = 'inline-deletelink';
+        addEvent(d_handle, 'click', function(){
+            show_loading();
+            xmlhttp.open('POST', 'delete-inline/'+name+'/'+id+'/', true);
+            function onStateChange() {
+                if (xmlhttp.readyState != 4) return;
+                hide_loading();
+                if ((xmlhttp.status >= 200) && (xmlhttp.status < 300)){
+                    var response;
+                    eval('response = '+xmlhttp.responseText);
+                    if(response.error) {
+                        alert(response.error);
+                        return;
+                    }
+                    var tc = $('id_'+name+'-TOTAL_FORMS');
+                    tc.value = Math.ceil(tc.value) - 1;
+                    var ic = $('id_'+name+'-INITIAL_FORMS');
+                    ic.value = Math.ceil(ic.value) - 1;
+                    var tbody = p.parentNode.parentNode;
+                    p.parentNode.parentNode.removeChild(p.parentNode);
+                    var trs = $$('tr', tbody);
+                    for(var i=0, l=trs.length; i<l; i++){
+                        trs[i].className = trs[i].className.replace(/row\d/, i % 2 ? 'row2' : 'row1');
+                        function fix_id_and_name(input) {
+                            input.name = input.name.replace(/-\d-/, '-'+i+'-');
+                            input.id = input.id.replace(/-\d-/, '-'+i+'-');
+                        }
+                        $$('input', trs[i]).map(fix_id_and_name);
+                    }
+                    
+                } else {
+                    // strange
+                }
+            }
+            xmlhttp.onreadystatechange = onStateChange;
+            xmlhttp.send(null);
+        })
+        p.appendChild(d_handle);
+    }
+
+    function make_add_link(group){
+        
+        var rows = $$('tr', group);
+        var origin = $$('td.original input', group)[0];
+        if(! origin) return;
+        var name = origin.name.match(/^(\w+)-/i)[1];
+        var tc = $('id_'+name+'-TOTAL_FORMS');
+        last_row = rows[rows.length-1];
+        var tmp_row = document.createElement('tr');
+        var tds = $$('td', last_row);
+        for(var i=0, l=tds.length; i<l; i++){
+            var td = tmp_row.appendChild(document.createElement('td'));
+            td.className = tds[i].className;
+            var inputs = $$('input, textarea', tds[i]);
+            for(var j=0, k=inputs.length; j<k; j++){
+                var o_input = inputs[j];            
+                var input = td.appendChild(document.createElement(o_input.nodeName));
+                input.type = o_input.type;
+                input.id = o_input.id.replace(/-\d-/, '---');
+                input.name = o_input.name.replace(/-\d-/, '---');
+            }
+        }
+        
+        function add_new() {
+            var tbody = $$('tbody', group)[0];
+            var tr = document.createElement('tr');
+            var tds = $$('td', tmp_row);
+            tc.value = Math.ceil(tc.value) + 1;
+            var id = tc.value - 1;
+            for(var i=0, l=tds.length; i<l; i++){
+                var td = tr.appendChild(document.createElement('td'));
+                td.className = tds[i].className;
+                var inputs = $$('input, textarea', tds[i]);
+                for(var j=0, k=inputs.length; j<k; j++){
+                    var o_input = inputs[j];
+                    var input = td.appendChild(document.createElement(o_input.nodeName));
+                    input.type = o_input.type;
+                    input.id = o_input.id.replace(/---/, '-'+id+'-');
+                    input.name = o_input.name.replace(/---/, '-'+id+'-');
+                }
+            }
+            tr.className = id % 2 ? 'row2' : 'row1';
+            tbody.appendChild(tr);
+        }
+        
+        var link = document.createElement('a');
+        link.className = 'addlink';
+        link.href = 'javascript:(function(){})()';
+        link.appendChild(document.createTextNode(gettext('Add another')));
+        addEvent(link, 'click', add_new);
+        group.appendChild(link);
+        
+        tc.value = $$('tr.row1, tr.row2', group).length;
+    }
+
+    function init_tabular_deleting() {
+        var trs = $$('div.inline-related.tabular thead tr');
+        for(var i=0, l=trs.length; i<l; i++) {
+            var ths = $$('th', trs[i]);
+            ths[ths.length-1].firstChild.data = '';
+            ths[ths.length-1].style.borderLeft = 'none';
+        }
+        $$('div.inline-related.tabular').map(make_add_link);
+        $$('div.inline-related.tabular td.delete input').map(make_delete_handle);
+    }
+
+    addEvent(window, 'load', init_tabular_deleting);
+
+})();
\ No newline at end of file
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 7997)
+++ django/contrib/admin/options.py	(working copy)
@@ -248,6 +248,8 @@
             return self.history_view(request, unquote(url[:-8]))
         elif url.endswith('delete'):
             return self.delete_view(request, unquote(url[:-7]))
+        elif url.count('delete-inline'):
+            return self.delete_inline_view(request, unquote(url))
         else:
             return self.change_view(request, unquote(url))
 
@@ -667,6 +669,57 @@
             "admin/delete_confirmation.html"
         ], context, context_instance=template.RequestContext(request))
 
+    def delete_inline_view(self, request, rest, extra_context=None):
+        """ 'delete-inline' view for this model """
+        
+        from django.http import HttpResponse
+        from django.utils import simplejson
+        from django.core.exceptions import ObjectDoesNotExist
+        
+        def json(data):
+            return HttpResponse(simplejson.dumps(data), mimetype='text/x-json')
+        
+        try:
+            parent_id, trash, set_name, child_id = filter(None, rest.split('/'))
+        except ValueError:
+            return json({'error': _('Invalid request.')})
+        
+        if request.method != 'POST':
+            return json({'error': _('Request method should be post.')})
+        
+        from django.contrib.admin.models import LogEntry, DELETION
+        opts = self.model._meta
+        app_label = opts.app_label
+
+        try:
+            parent = self.model._default_manager.get(pk=parent_id)
+        except self.model.DoesNotExist:
+            parent = None
+
+        try:
+            _set = getattr(parent, set_name)
+        except AttributeError:
+            return json({'error': _('Unknown "%s" for "%s".') % (set_name, self.model)})
+
+        try:
+            obj = _set.get(pk=child_id)
+        except ObjectDoesNotExist:
+            obj = None
+            
+        if obj is None:
+            return json({'error': _('Object does not exists.')})
+                
+        perms_needed = sets.Set()
+        get_deleted_objects([], perms_needed, request.user, obj, obj._meta, 1, self.admin_site)
+        
+        if perms_needed:
+            return json({'error': _('Not enought permissions to delete related objects.')})
+        
+        obj.delete()
+        LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(obj.__class__).id, obj, str(obj), DELETION)
+
+        return json({'error': None})
+
     def history_view(self, request, object_id, extra_context=None):
         "The 'history' admin view for this model."
         from django.contrib.admin.models import LogEntry
