diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 04a3492..98efe5a 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -1,4 +1,5 @@
 from django import forms
+from django.core.urlresolvers import reverse
 from django.contrib.admin.util import (flatten_fieldsets, lookup_field,
     display_for_field, label_for_field, help_text_for_field)
 from django.contrib.admin.templatetags.admin_static import static
@@ -189,6 +190,7 @@ class AdminReadonlyField(object):
                     result_repr = display_for_field(value, f)
         return conditional_escape(result_repr)
 
+
 class InlineAdminFormSet(object):
     """
     A wrapper around an inline formset for use in the admin system.
@@ -251,9 +253,16 @@ class InlineAdminForm(AdminForm):
         self.formset = formset
         self.model_admin = model_admin
         self.original = original
+        self.admin_url = None
         if original is not None:
             self.original_content_type_id = ContentType.objects.get_for_model(original).pk
-        self.show_url = original and hasattr(original, 'get_absolute_url')
+            self.show_url = hasattr(original, 'get_absolute_url')
+            if (model_admin is not None and
+                    original.__class__ in model_admin.admin_site._registry):
+                info = (original._meta.app_label, original._meta.object_name.lower())
+                self.admin_url = reverse('admin:%s_%s_change' % info, args=(original.pk,),
+                        current_app=model_admin.admin_site.name)
+
         super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
             readonly_fields, model_admin)
 
diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js
index 94aa6db..80d2d91 100644
--- a/django/contrib/admin/static/admin/js/actions.js
+++ b/django/contrib/admin/static/admin/js/actions.js
@@ -136,4 +136,30 @@
 		allToggle: "#action-toggle",
 		selectedClass: "selected"
 	}
+
+	$(function(){
+		/* Change the edit link next to foreign key selects
+			according to the selected value */
+		$("a.change-related").click(function(e){
+			e.preventDefault();
+			window.open($(this).attr("href"));
+		});
+		$("a.change-related").each(
+			function(i, el){
+				var elem = $(el),
+					defaultHref = elem.attr("href"),
+					selectId = elem.attr("id").split("_").slice(1).join("_");
+				$("#"+selectId).change(function(){
+					var val = $(this).val();
+					if (val === ""){
+						elem.hide();
+						return;
+					}
+					elem.show();
+					var newHref = defaultHref.replace(/\/\d+\/$/, "/" + val + "/");
+					elem.attr("href", newHref);
+				});
+			}
+		);
+	});
 })(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/actions.min.js b/django/contrib/admin/static/admin/js/actions.min.js
index 21f00cd..5689fcc 100644
--- a/django/contrib/admin/static/admin/js/actions.min.js
+++ b/django/contrib/admin/static/admin/js/actions.min.js
@@ -1,7 +1,7 @@
-(function(a){a.fn.actions=function(h){var b=a.extend({},a.fn.actions.defaults,h),e=a(this),f=false;checker=function(c){c?showQuestion():reset();a(e).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).attr("checked",function(){if(c==e.length){value=true;showQuestion()}else{value=
-false;clearAcross()}return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};
-a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();a(b.acrossInput).val()==1&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",false);clearAcross();checker(0);
-updateCounter()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey==true){var g=false;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))g=g?false:true;g&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,
-d.checked);lastChecked=d;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){f=true});a('form#changelist-form button[name="index"]').click(function(){if(f)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var c=false;a("div.actions select option:selected").each(function(){if(a(this).val())c=
-true});if(c)return f?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",
-acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery);
+(function(a){a.fn.actions=function(g){var b=a.extend({},a.fn.actions.defaults,g),d=a(this),e=!1;checker=function(c){c?showQuestion():reset();a(d).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(d).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).attr("checked",function(){c==d.length?(value=!0,showQuestion()):(value=
+!1,clearAcross());return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};
+a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();a(b.acrossInput).val()==1&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",!1);clearAcross();checker(0);
+updateCounter()});lastChecked=null;a(d).click(function(c){if(!c)c=window.event;var f=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(f)&&c.shiftKey==!0){var e=!1;a(lastChecked).attr("checked",f.checked).parent().parent().toggleClass(b.selectedClass,f.checked);a(d).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(f))e=e?!1:!0;e&&a(this).attr("checked",f.checked).parent().parent().toggleClass(b.selectedClass,f.checked)})}a(f).parent().parent().toggleClass(b.selectedClass,
+f.checked);lastChecked=f;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var b=!1;a("div.actions select option:selected").each(function(){a(this).val()&&(b=!0)});
+if(b)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",
+acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"};a(function(){a("a.change-related").click(function(g){g.preventDefault();window.open(a(this).attr("href"))});a("a.change-related").each(function(g,b){var d=a(b),e=d.attr("href"),c=d.attr("id").split("_").slice(1).join("_");a("#"+c).change(function(){var b=a(this).val();b===""?d.hide():(d.show(),b=e.replace(/\/\d+\/$/,"/"+b+"/"),d.attr("href",b))})})})})(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 476e261..cf6723b 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -6,6 +6,7 @@
 
 {% 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>
+    {% if inline_admin_form.admin_url %}<a href="{{ inline_admin_form.admin_url }}" class="inline-changelink changelink">{% trans "edit separately" %}</a>&nbsp;&nbsp;{% endif %}
     {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% 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 %}
   </h3>
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index 71b097e..4cc9a55 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -27,6 +27,7 @@
         <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 %}
+          {% if inline_admin_form.admin_url %}<a href="{{ inline_admin_form.admin_url }}" class="inline-changelink changelink">{% trans "edit separately" %}</a>&nbsp;&nbsp;{% endif %}
           {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
             </p>{% endif %}
           {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 0d1f2a9..4ce30ae 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -217,8 +217,8 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
 
 class RelatedFieldWidgetWrapper(forms.Widget):
     """
-    This class is a wrapper to a given widget to add the add icon for the
-    admin interface.
+    This class is a wrapper to a given widget to add the add icon and the
+    change icon for the admin interface.
     """
     def __init__(self, widget, rel, admin_site, can_add_related=None):
         self.is_hidden = widget.is_hidden
@@ -259,6 +259,21 @@ class RelatedFieldWidgetWrapper(forms.Widget):
                           % (related_url, name))
             output.append(u'<img src="%s" width="10" height="10" alt="%s"/></a>'
                           % (static('admin/img/icon_addlink.gif'), _('Add Another')))
+
+            if not self.widget.allow_multiple_selected:
+                if value is None:
+                    val = 0
+                    display = ' style="display: none"'
+                else:
+                    val = value
+                    display = ''
+                related_change_url = reverse('admin:%s_%s_change' % info, args=(val,),
+                        current_app=self.admin_site.name)
+                output.append(u'<a href="%s" class="change-related" id="change_id_%s"%s>'
+                              % (related_change_url, name, display))
+                output.append(u'<img src="%s" width="10" height="10" alt="%s"/></a>'
+                              % (static('admin/img/icon_changelink.gif'), _('Change')))
+
         return mark_safe(u''.join(output))
 
     def build_attrs(self, extra_attrs=None, **kwargs):
diff --git a/tests/regressiontests/admin_inlines/admin.py b/tests/regressiontests/admin_inlines/admin.py
index 4edd361..43d7232 100644
--- a/tests/regressiontests/admin_inlines/admin.py
+++ b/tests/regressiontests/admin_inlines/admin.py
@@ -109,6 +109,21 @@ class SottoCapoInline(admin.TabularInline):
     model = SottoCapo
 
 
+class IndividualInline(admin.StackedInline):
+    model = Individual
+    extra = 1
+
+
+class PhoneInline(admin.StackedInline):
+    model = Phone
+    extra = 1
+
+
+class EmailInline(admin.TabularInline):
+    model = Email
+    extra = 1
+
+
 site.register(TitleCollection, inlines=[TitleInline])
 # Test bug #12561 and #12778
 # only ModelAdmin media
@@ -124,3 +139,8 @@ site.register(Fashionista, inlines=[InlineWeakness])
 site.register(Holder4, Holder4Admin)
 site.register(Author, AuthorAdmin)
 site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
+
+site.register(Household, inlines=[IndividualInline, PhoneInline])
+site.register(Individual, inlines=[EmailInline])
+site.register(Email)
+# (Phone not registered)
diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py
index 748280d..896b3d0 100644
--- a/tests/regressiontests/admin_inlines/models.py
+++ b/tests/regressiontests/admin_inlines/models.py
@@ -136,3 +136,23 @@ class Consigliere(models.Model):
 class SottoCapo(models.Model):
     name = models.CharField(max_length=100)
     capo_famiglia = models.ForeignKey(CapoFamiglia, related_name='+')
+
+
+# Test InlineAdminForm.admin_url:
+
+class Household(models.Model):
+    pass
+
+
+class Individual(models.Model):
+    household = models.ForeignKey(Household)
+
+
+class Phone(models.Model):
+    household = models.ForeignKey(Household)
+    number = models.CharField(max_length=64)
+
+
+class Email(models.Model):
+    individual = models.ForeignKey(Individual)
+    email = models.EmailField()
diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
index 955d620..02c8442 100644
--- a/tests/regressiontests/admin_inlines/tests.py
+++ b/tests/regressiontests/admin_inlines/tests.py
@@ -1,3 +1,4 @@
+from django.core import urlresolvers
 from django.contrib.admin.helpers import InlineAdminForm
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
@@ -5,8 +6,8 @@ from django.test import TestCase
 # local test models
 from models import (Holder, Inner, Holder2, Inner2, Holder3,
     Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child,
-    CapoFamiglia, Consigliere, SottoCapo)
-from admin import InnerInline
+    CapoFamiglia, Consigliere, SottoCapo, Household, Individual, Phone, Email)
+from admin import site, InnerInline
 
 
 class TestInline(TestCase):
@@ -196,3 +197,86 @@ class TestInlineAdminForm(TestCase):
         iaf = InlineAdminForm(None, None, {}, {}, joe)
         parent_ct = ContentType.objects.get_for_model(Parent)
         self.assertEqual(iaf.original.content_type, parent_ct)
+
+
+class TestAdminURL(TestCase):
+    urls = "regressiontests.admin_inlines.urls"
+    fixtures = ['admin-views-users.xml']
+
+    def get_admin_url(self, obj_or_class, add=False):
+        params = [site.name, obj_or_class._meta.app_label,
+                obj_or_class._meta.module_name]
+        if add:
+            params.append("add")
+            args = ()
+        else:
+            params.append("change")
+            args = (obj_or_class.pk,)
+        return urlresolvers.reverse('%s:%s_%s_%s' % tuple(params), args=args)
+
+    def setUp(self):
+        self.household = Household.objects.create()
+        self.individual = Individual.objects.create(household=self.household)
+        self.phone = Phone.objects.create(household=self.household,
+                                          number='1234567890')
+        self.email = Email.objects.create(individual=self.individual,
+                                          email='me@example.com')
+
+        result = self.client.login(username='super', password='secret')
+        self.assertEqual(result, True)
+
+    def tearDown(self):
+        self.client.logout()
+
+    def test_admin_url(self):
+        """
+        admin_url should be set for admin-registered inline models only.
+
+        Also check to ensure URLs look correct and only set on bound forms.
+        """
+        admin_url = self.get_admin_url(self.household)
+        response = self.client.get(admin_url)
+        for inline_admin_fset in response.context[-1]['inline_admin_formsets']:
+            for inline_admin_form in inline_admin_fset:
+                if inline_admin_form.form._meta.model != Individual:
+                    self.assertFalse(
+                        getattr(inline_admin_form, 'admin_url', None),
+                        'admin_url set with unregistered model')
+                elif not inline_admin_form.original:
+                    self.assertFalse(
+                        getattr(inline_admin_form, 'admin_url', None),
+                        'admin_url set on unbound form!')
+                else:
+                    self.assertTrue(inline_admin_form.admin_url,
+                                    'admin_url not set')
+                    self.assertEqual(
+                        inline_admin_form.original, self.individual,
+                        'original is not expected object')
+                    self.assertEqual(
+                        inline_admin_form.admin_url,
+                        self.get_admin_url(inline_admin_form.original),
+                        'admin_url appears incorrect')
+
+    def test_link_rendering(self):
+        """ Confirm links are displayed where appropriate. """
+        LINK_CSS_CLASS = 'inline-changelink'
+        LINK_TEXT = 'edit separately'
+
+        # test StackedInline rendering
+        response = self.client.get(self.get_admin_url(Household, 'add'))
+        self.assertNotContains(response, LINK_CSS_CLASS)
+        self.assertNotContains(response, LINK_TEXT)
+
+        response = self.client.get(self.get_admin_url(self.household))
+        self.assertContains(response, LINK_CSS_CLASS)
+        self.assertContains(response, LINK_TEXT)
+
+        # test TabularInline rendering
+
+        response = self.client.get(self.get_admin_url(Individual, 'add'))
+        self.assertNotContains(response, LINK_CSS_CLASS)
+        self.assertNotContains(response, LINK_TEXT)
+
+        response = self.client.get(self.get_admin_url(self.individual))
+        self.assertContains(response, LINK_CSS_CLASS)
+        self.assertContains(response, LINK_TEXT)
diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py
index e4aae4f..3d47ab6 100644
--- a/tests/regressiontests/admin_views/admin.py
+++ b/tests/regressiontests/admin_views/admin.py
@@ -385,6 +385,14 @@ class PizzaAdmin(admin.ModelAdmin):
     readonly_fields = ('toppings',)
 
 
+class DonutOrderAdmin(admin.ModelAdmin):
+    raw_id_fields = ('donut',)
+
+
+class PizzaOrderAdmin(admin.ModelAdmin):
+    readonly_fields = ('pizza',)
+
+
 class WorkHourAdmin(admin.ModelAdmin):
     list_display = ('datum', 'employee')
     list_filter = ('employee',)
@@ -486,6 +494,7 @@ site.register(Post, PostAdmin)
 site.register(Gadget, GadgetAdmin)
 site.register(Villain)
 site.register(SuperVillain)
+site.register(Ambush)
 site.register(Plot)
 site.register(PlotDetails)
 site.register(CyclicOne)
@@ -512,7 +521,10 @@ site.register(Book, inlines=[ChapterInline])
 site.register(Promo)
 site.register(ChapterXtra1, ChapterXtra1Admin)
 site.register(Pizza, PizzaAdmin)
+site.register(PizzaOrder, PizzaOrderAdmin)
 site.register(Topping)
+site.register(Donut)
+site.register(DonutOrder, DonutOrderAdmin)
 site.register(Album, AlbumAdmin)
 site.register(Question)
 site.register(Answer)
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index bb8d026..f04dfea 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -433,6 +433,14 @@ class SuperSecretHideout(models.Model):
         return self.location
 
 
+class Ambush(models.Model):
+    hideout = models.ForeignKey(SecretHideout)
+    when = models.DateTimeField()
+
+    def __unicode__(self):
+        return u'Ambush %s at %s' % (self.hideout, self.when)
+
+
 class CyclicOne(models.Model):
     name = models.CharField(max_length=25)
     two = models.ForeignKey('CyclicTwo')
@@ -458,6 +466,30 @@ class Pizza(models.Model):
     toppings = models.ManyToManyField('Topping')
 
 
+class Donut(models.Model):
+    name = models.CharField(max_length=20)
+    toppings = models.ManyToManyField('Topping')
+
+    def __unicode__(self):
+        return self.name
+
+
+class DonutOrder(models.Model):
+    donut = models.ForeignKey(Donut)
+    quantity = models.IntegerField()
+
+    def __unicode__(self):
+        return '%s x %d' % (self.donut, self.quantity)
+
+
+class PizzaOrder(models.Model):
+    pizza = models.ForeignKey(Pizza)
+    quantity = models.IntegerField()
+
+    def __unicode__(self):
+        return '%s x %d' % (self.pizza, self.quantity)
+
+
 class Album(models.Model):
     owner = models.ForeignKey(User)
     title = models.CharField(max_length=30)
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index b6e7b9e..7ce9e61 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -38,7 +38,9 @@ from models import (Article, BarAccount, CustomArticle, EmptyModel,
     Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
     Question, Answer, Inquisition, Actor, FoodDelivery,
     RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory,
-    ComplexSortedPerson, Parent, Child)
+    ComplexSortedPerson, Parent, Child, PlotDetails, Villain, SecretHideout,
+    Ambush, Pizza, PizzaOrder, Topping, Donut, DonutOrder)
+
 
 ERROR_MESSAGE = "Please enter the correct username and password \
 for a staff account. Note that both fields are case-sensitive."
@@ -3255,3 +3257,108 @@ class AdminCustomSaveRelatedTests(TestCase):
 
         self.assertEqual('Josh Stone', Parent.objects.latest('id').name)
         self.assertEqual([u'Catherine Stone', u'Paul Stone'], children_names)
+
+
+class RelatedLinksTest(TestCase):
+    urls = "regressiontests.admin_views.urls"
+    fixtures = ['admin-views-users.xml']
+
+    def setUp(self):
+        self.client.login(username='super', password='secret')
+        self.RELATED_LINK_CSS_CLASS = 'change-related'
+        self.READONLY_LINK_CSS_CLASS = 'readonly-changelink'
+        self.black_knight = Villain.objects.create(name='Black Knight')
+        self.plot = Plot.objects.create(name='None shall <pass>',
+                                        team_leader=self.black_knight,
+                                        contact=self.black_knight)
+        self.plot_details = PlotDetails.objects.create(
+            plot=self.plot, details="I'll bite your legs off!")
+        self.hideout = SecretHideout.objects.create(villain=self.black_knight,
+                                                    location='forest')
+        self.ambush = Ambush.objects.create(hideout=self.hideout,
+                                            when=datetime.datetime.now())
+
+        self.pizza = Pizza.objects.create(name='Wafer-thin pizza')
+        self.topping = Topping.objects.create(name='Mint')
+        self.pizza.toppings.add(self.topping)
+        self.donut = Donut.objects.create(name='Wafer-thin donut')
+        self.donut.toppings.add(self.topping)
+        self.donut_order = DonutOrder.objects.create(donut=self.donut,
+                                                     quantity=1000000)
+        self.pizza_order = PizzaOrder.objects.create(pizza=self.pizza,
+                                                     quantity=50)
+
+    def tearDown(self):
+        self.client.logout()
+
+    def test_foreignkey(self):
+        """ Confirm changelink appears in ForeignKey fields """
+        response = self.client.get('/test_admin/admin/admin_views/plot/add/')
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS)
+        self.assertContains(response, '/0/"')
+        response = self.client.get('/test_admin/admin/admin_views/plot/%d/' %
+                                   self.plot.pk)
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS)
+
+    def test_onetoone(self):
+        """ Confirm changelink appears in populated OneToOne fields """
+        response = self.client.get(
+            '/test_admin/admin/admin_views/plotdetails/add/')
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/plotdetails/%d/' %
+            self.plot_details.pk)
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS)
+
+    def test_unregistered(self):
+        """ Confirm changelink does *not* appear for unregistered fields """
+        response = self.client.get(
+            '/test_admin/admin/admin_views/ambush/add/')
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/ambush/%d/' %
+            self.ambush.pk)
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+
+    def test_readonly(self):
+        """ Confirm changelink does not appear for populated readonly fields """
+        response = self.client.get(
+            '/test_admin/admin/admin_views/pizzaorder/add/')
+        self.assertNotContains(response, self.READONLY_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/pizzaorder/%d/' %
+            self.pizza_order.pk)
+        self.assertNotContains(response, self.READONLY_LINK_CSS_CLASS)
+
+    def test_manytomany(self):
+        """ Confirm changelinks do *not* appear for ManyToMany fields """
+        MULTIPLE_SELECT_STRING = '<select multiple="multiple"'
+
+        response = self.client.get(
+            '/test_admin/admin/admin_views/pizza/add/')
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        self.assertNotContains(response, MULTIPLE_SELECT_STRING)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/pizza/%d/' % self.pizza.pk)
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        self.assertNotContains(response, MULTIPLE_SELECT_STRING)
+
+        # try non-readonly ManyToMany
+        response = self.client.get(
+            '/test_admin/admin/admin_views/donut/add/')
+        self.assertContains(response, MULTIPLE_SELECT_STRING)
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/donut/%d/' % self.donut.pk)
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        self.assertContains(response, MULTIPLE_SELECT_STRING)
+
+    def test_raw_id(self):
+        """ Confirm links do not appear for populated raw_id_fields """
+        response = self.client.get(
+            '/test_admin/admin/admin_views/donutorder/add/')
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/donutorder/%d/' %
+            self.donut_order.pk)
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
\ No newline at end of file
