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/options.py b/django/contrib/admin/options.py
index f05b5cb..b2c8411 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -155,15 +155,23 @@ class BaseModelAdmin(object):
         Get a form Field for a ForeignKey.
         """
         db = kwargs.get('using')
+        related_modeladmin = self.admin_site._registry.get(
+                                                    db_field.rel.to)
+        can_change_related = bool(related_modeladmin and
+                    related_modeladmin.has_change_permission(request))
         if db_field.name in self.raw_id_fields:
             kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel,
                                     self.admin_site, using=db)
         elif db_field.name in self.radio_fields:
-            kwargs['widget'] = widgets.AdminRadioSelect(attrs={
-                'class': get_ul_class(self.radio_fields[db_field.name]),
-            })
+            kwargs['widget'] = widgets.AdminRadioSelect(db_field.rel,
+                    self.admin_site, can_change_related=can_change_related,
+                    attrs={
+                        'class': get_ul_class(self.radio_fields[db_field.name]),
+                    })
             kwargs['empty_label'] = db_field.blank and _('None') or None
-
+        else:
+            kwargs['widget'] = widgets.AdminSelect(db_field.rel,
+                    self.admin_site, can_change_related=can_change_related)
         return db_field.formfield(**kwargs)
 
     def formfield_for_manytomany(self, db_field, request=None, **kwargs):
@@ -357,9 +365,9 @@ class ModelAdmin(BaseModelAdmin):
     def media(self):
         js = [
             'core.js',
-            'admin/RelatedObjectLookups.js',
             'jquery.min.js',
-            'jquery.init.js'
+            'jquery.init.js',
+            'admin/RelatedObjectLookups.js'
         ]
         if self.actions is not None:
             js.append('actions.min.js')
diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
index 1bc78f8..90f746a 100644
--- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
+++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
@@ -94,3 +94,36 @@ function dismissAddAnotherPopup(win, newId, newRepr) {
     }
     win.close();
 }
+
+(function($){
+    $(function(){
+        /* Open change related links in new tab by default */
+        var openInNewTab = function(e){
+            e.preventDefault();
+            window.open($(this).attr("href"));
+        };
+        $("a.change-related").click(openInNewTab);
+        $("a.change-related-radio").click(openInNewTab);
+
+        /* Adapt the change link next to foreign key selects
+            and raw id text fields according to the selected value */
+        $("a.change-related").each(
+            function(i, el){
+                var elem = $(el),
+                    defaultHref = elem.attr("href"),
+                    selectId = elem.attr("id").split("_").slice(1).join("_");
+                // TODO: bind to more than one
+                $("#"+selectId).bind("change keyup", function(){
+                    var val = $(this).val();
+                    if (!(/^\d+$/.test(val))){
+                        elem.css("visibility", "hidden");
+                        return;
+                    }
+                    elem.css("visibility", "visible");
+                    var newHref = defaultHref.replace(/\/\d+\/$/, "/" + val + "/");
+                    elem.attr("href", newHref);
+                });
+            }
+        );
+    });
+}(django.jQuery));
\ No newline at end of file
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..a3e6190 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -80,17 +80,79 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
         return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \
             (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1]))
 
+def render_change_related_link(rel_to, admin_site, name, value,
+        class_name=u"change-related", id_postfix=u"%s", extra=None):
+    """
+    Renders change related links by reversing the admin url
+    and generating some html according to the widget's needs.
+    """
+    template = u'<a href="%s" class="%s" id="change_id_%s"%s><img src="%s" width="10" height="10" alt="%s"/></a>'
+    info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
+    change_related_url = reverse('admin:%s_%s_change' % info,
+            args=(0 if value is None else value,),
+            current_app=admin_site.name)
+    id_postfix = id_postfix % name
+    if extra is None and not value:
+        extra = u' style="visibility:hidden"'
+    else:
+        extra = u''
+    return mark_safe(template % (change_related_url, class_name, id_postfix,
+            extra, static('admin/img/icon_changelink.gif'), _('Change')))
+
+class AdminSelect(forms.Select):
+    def __init__(self, rel, admin_site, can_change_related=None, attrs=None):
+        self.rel = rel
+        self.admin_site = admin_site
+        # Backwards compatible check for whether a user can change related
+        # objects.
+        if can_change_related is None:
+            can_change_related = rel.to in admin_site._registry
+        self.can_change_related = can_change_related
+        super(AdminSelect, self).__init__(attrs)
+
+    def render(self, name, value, attrs=None, choices=()):
+        output = [super(AdminSelect, self).render(name, value,
+            attrs=attrs, choices=choices)]
+        if self.can_change_related:
+            output.append(render_change_related_link(self.rel.to,
+                    self.admin_site,name, value))
+        return mark_safe(u''.join(output))
+
 class AdminRadioFieldRenderer(RadioFieldRenderer):
-    def render(self):
+
+    def render(self, rel_to=None, admin_site=None):
         """Outputs a <ul> for this set of radio fields."""
-        return mark_safe(u'<ul%s>\n%s\n</ul>' % (
-            flatatt(self.attrs),
-            u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self]))
-        )
+        if rel_to and admin_site:
+            link = lambda w: render_change_related_link(rel_to,
+                    admin_site, w.name, w.choice_value,
+                    class_name=u'change-related-radio',
+                    id_postfix=u"%%s_%s" % w.index)
+        else:
+            link = lambda w: u""
+        return mark_safe(u'<ul%s>\n%s\n</ul>' % (flatatt(self.attrs),
+                u'\n'.join([u'<li>%s%s</li>' % (
+                    force_unicode(w), link(w)) for w in self])))
 
 class AdminRadioSelect(forms.RadioSelect):
     renderer = AdminRadioFieldRenderer
 
+    def __init__(self, rel=None, admin_site=None, **kwargs):
+        self.rel = rel
+        self.admin_site = admin_site
+        # Backwards compatible check for whether a user can change related
+        # objects.
+        can_change_related = kwargs.pop('can_change_related', None)
+        if can_change_related is None and rel and admin_site:
+            can_change_related = rel.to in admin_site._registry
+        self.can_change_related = can_change_related
+        super(AdminRadioSelect, self).__init__(**kwargs)
+
+    def render(self, name, value, attrs=None, choices=()):
+        renderer = self.get_renderer(name, value, attrs, choices)
+        if self.can_change_related:
+            return renderer.render(self.rel.to, self.admin_site)
+        return renderer.render()
+
 class AdminFileWidget(forms.ClearableFileInput):
     template_with_initial = (u'<p class="file-upload">%s</p>'
                             % forms.ClearableFileInput.template_with_initial)
@@ -122,10 +184,19 @@ class ForeignKeyRawIdWidget(forms.TextInput):
     A Widget for displaying ForeignKeys in the "raw_id" interface rather than
     in a <select> box.
     """
-    def __init__(self, rel, admin_site, attrs=None, using=None):
+
+    allow_multiple_selected = False
+
+    def __init__(self, rel, admin_site, attrs=None, can_change_related=None,
+            using=None):
         self.rel = rel
         self.admin_site = admin_site
         self.db = using
+        # Backwards compatible check for whether a user can change related
+        # objects.
+        if can_change_related is None:
+            can_change_related = rel.to in admin_site._registry
+        self.can_change_related = can_change_related
         super(ForeignKeyRawIdWidget, self).__init__(attrs)
 
     def render(self, name, value, attrs=None):
@@ -134,6 +205,9 @@ class ForeignKeyRawIdWidget(forms.TextInput):
             attrs = {}
         extra = []
         if rel_to in self.admin_site._registry:
+            if self.can_change_related and not self.allow_multiple_selected:
+                extra.append(render_change_related_link(rel_to, self.admin_site,
+                        name, value))
             # The related object is registered with the same AdminSite
             related_url = reverse('admin:%s_%s_changelist' %
                                     (rel_to._meta.app_label,
@@ -180,6 +254,9 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
     A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
     in a <select multiple> box.
     """
+
+    allow_multiple_selected = True
+
     def render(self, name, value, attrs=None):
         if attrs is None:
             attrs = {}
@@ -217,8 +294,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
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..c07ee8a 100644
--- a/tests/regressiontests/admin_views/admin.py
+++ b/tests/regressiontests/admin_views/admin.py
@@ -385,6 +385,22 @@ class PizzaAdmin(admin.ModelAdmin):
     readonly_fields = ('toppings',)
 
 
+class PizzaOrderAdmin(admin.ModelAdmin):
+    readonly_fields = ('pizza',)
+
+
+class DonutOrderAdmin(admin.ModelAdmin):
+    raw_id_fields = ('donut',)
+
+
+class KebapAdmin(admin.ModelAdmin):
+    raw_id_fields = ('toppings',)
+
+
+class KebapOrderAdmin(admin.ModelAdmin):
+    radio_fields = {'kebap': admin.VERTICAL}
+
+
 class WorkHourAdmin(admin.ModelAdmin):
     list_display = ('datum', 'employee')
     list_filter = ('employee',)
@@ -486,6 +502,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 +529,12 @@ 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(Kebap, KebapAdmin)
+site.register(KebapOrder, KebapOrderAdmin)
 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..49defd8 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,46 @@ class Pizza(models.Model):
     toppings = models.ManyToManyField('Topping')
 
 
+class PizzaOrder(models.Model):
+    pizza = models.ForeignKey(Pizza)
+    quantity = models.IntegerField()
+
+    def __unicode__(self):
+        return '%s x %d' % (self.pizza, self.quantity)
+
+
+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 Kebap(models.Model):
+    name = models.CharField(max_length=20)
+    toppings = models.ManyToManyField('Topping')
+
+    def __unicode__(self):
+        return self.name
+
+
+class KebapOrder(models.Model):
+    kebap = models.ForeignKey(Kebap)
+    quantity = models.IntegerField()
+
+    def __unicode__(self):
+        return '%s x %d' % (self.kebap, 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..588b4f9 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, Kebap, KebapOrder)
+
 
 ERROR_MESSAGE = "Please enter the correct username and password \
 for a staff account. Note that both fields are case-sensitive."
@@ -3255,3 +3257,132 @@ 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.RELATED_LINK_CSS_CLASS_RADIO = 'change-related-radio'
+        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)
+        self.kebap = Kebap.objects.create(name='Wafer-thin kebap')
+        self.kebap.toppings.add(self.topping)
+        self.kebap_order = KebapOrder.objects.create(kebap=self.kebap,
+                                                     quantity=42)
+
+    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.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/pizzaorder/%d/' %
+            self.pizza_order.pk)
+        self.assertNotContains(response, self.RELATED_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.assertContains(response, self.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/donutorder/%d/' %
+            self.donut_order.pk)
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS)
+
+    def test_radio_fields(self):
+        """ Confirm links appear for foreign keyradio fields """
+        response = self.client.get(
+            '/test_admin/admin/admin_views/kebaporder/add/')
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS_RADIO)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/kebaporder/%d/' %
+            self.kebap_order.pk)
+        self.assertContains(response, self.RELATED_LINK_CSS_CLASS_RADIO)
+
+    def test_raw_id_manytomany(self):
+        """ Confirm links do not appear for populated raw_id_fields """
+        response = self.client.get(
+            '/test_admin/admin/admin_views/kebap/add/')
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
+        response = self.client.get(
+            '/test_admin/admin/admin_views/kebap/%d/' %
+            self.kebap.pk)
+        self.assertNotContains(response, self.RELATED_LINK_CSS_CLASS)
\ No newline at end of file
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
index a7bfe55..48055fc 100644
--- a/tests/regressiontests/admin_widgets/tests.py
+++ b/tests/regressiontests/admin_widgets/tests.py
@@ -255,7 +255,7 @@ class ForeignKeyRawIdWidgetTest(DjangoTestCase):
         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
         self.assertEqual(
             conditional_escape(w.render('test', band.pk, attrs={})),
-            '<input type="text" name="test" value="%(bandpk)s" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Linkin Park</strong>' % dict(admin_media_prefix(), bandpk=band.pk)
+            '<input type="text" name="test" value="%(bandpk)s" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/band/1/" class="change-related" id="change_id_test"><img src="/static/admin/img/icon_changelink.gif" width="10" height="10" alt="Change"/></a><a href="/widget_admin/admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Linkin Park</strong>' % dict(admin_media_prefix(), bandpk=band.pk)
         )
 
     def test_relations_to_non_primary_key(self):
@@ -270,7 +270,7 @@ class ForeignKeyRawIdWidgetTest(DjangoTestCase):
         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
         self.assertEqual(
             w.render('test', core.parent_id, attrs={}),
-            '<input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Apple</strong>' % admin_media_prefix()
+            '<input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/86/" class="change-related" id="change_id_test"><img src="/static/admin/img/icon_changelink.gif" width="10" height="10" alt="Change"/></a><a href="/widget_admin/admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Apple</strong>' % admin_media_prefix()
         )
 
     def test_fk_related_model_not_in_admin(self):
@@ -312,7 +312,7 @@ class ForeignKeyRawIdWidgetTest(DjangoTestCase):
         )
         self.assertEqual(
             w.render('test', child_of_hidden.parent_id, attrs={}),
-            '<input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Hidden</strong>' % admin_media_prefix()
+            '<input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/93/" class="change-related" id="change_id_test"><img src="/static/admin/img/icon_changelink.gif" width="10" height="10" alt="Change"/></a><a href="/widget_admin/admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Hidden</strong>' % admin_media_prefix()
         )
 
 
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
index 872fb0c..c6f1929 100644
--- a/tests/regressiontests/modeladmin/tests.py
+++ b/tests/regressiontests/modeladmin/tests.py
@@ -6,7 +6,8 @@ from django.contrib.admin.options import (ModelAdmin, TabularInline,
     HORIZONTAL, VERTICAL)
 from django.contrib.admin.sites import AdminSite
 from django.contrib.admin.validation import validate
-from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
+from django.contrib.admin.widgets import (AdminDateWidget, AdminSelect,
+    AdminRadioSelect)
 from django.contrib.admin import (SimpleListFilter,
      BooleanFieldListFilter)
 from django.core.exceptions import ImproperlyConfigured
@@ -375,13 +376,13 @@ class ModelAdminTests(TestCase):
         cmafa = cma.get_form(request)
 
         self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget),
-            Select)
+            AdminSelect)
         self.assertEqual(
             list(cmafa.base_fields['main_band'].widget.choices),
             [(u'', u'---------'), (self.band.id, u'The Doors')])
 
         self.assertEqual(
-            type(cmafa.base_fields['opening_band'].widget.widget), Select)
+            type(cmafa.base_fields['opening_band'].widget.widget), AdminSelect)
         self.assertEqual(
             list(cmafa.base_fields['opening_band'].widget.choices),
             [(u'', u'---------'), (self.band.id, u'The Doors')])
