diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index aaa2e30..334db29 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -103,7 +103,8 @@ class InlineAdminFormSet(object):
 
     def __iter__(self):
         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
-            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
+            view_url = self.opts.get_view_on_site_url(original)
+            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original, view_url)
         for form in self.formset.extra_forms:
             yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
 
@@ -125,12 +126,13 @@ class InlineAdminForm(AdminForm):
     """
     A wrapper around an inline form for use in the admin system.
     """
-    def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
+    def __init__(self, formset, form, fieldsets, prepopulated_fields, original, view_on_site_url=None):
         self.formset = formset
         self.original = original
         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 = original and view_on_site_url is not None
+        self.absolute_url = view_on_site_url
         super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
     
     def __iter__(self):
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 3d60b9d..360504a 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -38,6 +38,7 @@ class BaseModelAdmin(object):
     filter_horizontal = ()
     radio_fields = {}
     prepopulated_fields = {}
+    view_on_site = False
 
     def formfield_for_dbfield(self, db_field, **kwargs):
         """
@@ -151,6 +152,26 @@ class BaseModelAdmin(object):
         return None
     declared_fieldsets = property(_declared_fieldsets)
 
+    def get_view_on_site_url(self, obj=None):
+        if obj is None or (hasattr(self, 'view_on_site') and not self.view_on_site):
+            return None
+
+        if callable(self.view_on_site):
+            return self.view_on_site(obj)
+        elif type(self.view_on_site) == bool and self.view_on_site:
+            # in case we have overriden the default view_on_site with a
+            # boolean flag, revert to the ContentType lookup
+            return self._view_on_site(obj)
+
+        return None
+
+    def _view_on_site(self, obj):
+        content_type_id, object_id = ContentType.objects.get_for_model(obj).pk, obj.pk
+
+        return "../../../r/%(content_type_id)s/%(object_id)s/" % ({
+            'content_type_id': content_type_id,
+            'object_id': object_id})
+
 class ModelAdmin(BaseModelAdmin):
     "Encapsulates all admin options and functionality for a given model."
     __metaclass__ = forms.MediaDefiningClass
@@ -392,7 +413,8 @@ class ModelAdmin(BaseModelAdmin):
             'has_change_permission': self.has_change_permission(request, obj),
             'has_delete_permission': self.has_delete_permission(request, obj),
             'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
-            'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
+            'has_absolute_url': self.get_view_on_site_url(obj) is not None,
+            'absolute_url': self.get_view_on_site_url(obj),
             'ordered_objects': ordered_objects,
             'form_url': mark_safe(form_url),
             'opts': opts,
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 2fb17bb..052d75e 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -25,7 +25,7 @@
 {% block object-tools %}
 {% if change %}{% if not is_popup %}
   <ul class="object-tools"><li><a href="history/" class="historylink">{% trans "History" %}</a></li>
-  {% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
+  {% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
   </ul>
 {% endif %}{% endif %}
 {% endblock %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 9d9f598..f43e7e0 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -10,7 +10,7 @@
     {% 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>
   {% if inline_admin_form.show_url %}
-  <p><a href="../../../r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a></p>
+  <p><a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a></p>
   {% endif %}
   {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
 
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index 820928a..7d6cc2f 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -24,7 +24,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.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_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
             </p>{% endif %}
           {{ inline_admin_form.pk_field.field }} {{ inline_admin_form.fk_field.field }}
           {% spaceless %}
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index ccade8a..faf70c7 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -232,6 +232,11 @@ def validate_base(cls, model):
             for idx, f in enumerate(val):
                 get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f)
 
+    # view_on_site
+    if hasattr(cls, 'view_on_site'):
+        if not callable(cls.view_on_site) and not isinstance(cls.view_on_site, bool):
+            raise ImproperlyConfigured("%s.view_on_site is not a callable or a boolean value." % cls.__name__)
+
 def check_isseq(cls, label, obj):
     if not isinstance(obj, (list, tuple)):
         raise ImproperlyConfigured("'%s.%s' must be a list or tuple." % (cls.__name__, label))
diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt
index f24dc46..3373f9c 100644
--- a/docs/ref/contrib/admin.txt
+++ b/docs/ref/contrib/admin.txt
@@ -597,6 +597,28 @@ with an operator:
     Performs a full-text match. This is like the default search method but uses
     an index. Currently this is only available for MySQL.
 
+``view_on_site``
+~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.1
+
+Set ``view_on_site`` to control whether to display or not the "View on site" link.
+This link should bring you to a URL where you can display the saved object.
+
+Example::
+
+    class PersonAdmin(admin.ModelAdmin):
+        view_on_site = True
+
+This value can be either a boolean flag or a callable. Default is ``False``.
+
+In case it is a callable, it accepts one parameter for the model instance.
+For example::
+
+    class PersonAdmin(admin.ModelAdmin):
+        def view_on_site(self, obj):
+            return '/show/%s/%s/' % (obj.first_name, obj.last_name)
+
 ``ModelAdmin`` methods
 ----------------------
 
diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-restaurants.xml b/tests/regressiontests/admin_views/fixtures/admin-views-restaurants.xml
new file mode 100644
index 0000000..fcb2efb
--- /dev/null
+++ b/tests/regressiontests/admin_views/fixtures/admin-views-restaurants.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+    <object pk="1" model="admin_views.city">
+        <field type="CharField" name="name">New York</field>
+    </object>
+    <object pk="2" model="admin_views.city">
+        <field type="CharField" name="name">Chicago</field>
+    </object>
+    <object pk="3" model="admin_views.city">
+        <field type="CharField" name="name">San Francisco</field>
+    </object>
+    <object pk="1" model="admin_views.restaurant">
+        <field to="admin_views.city" name="city" rel="ManyToOneRel">1</field>
+        <field type="CharField" name="name">Italian Pizza</field>
+    </object>
+    <object pk="2" model="admin_views.restaurant">
+        <field to="admin_views.city" name="city" rel="ManyToOneRel">1</field>
+        <field type="CharField" name="name">Boulevard</field>
+    </object>
+    <object pk="3" model="admin_views.restaurant">
+        <field to="admin_views.city" name="city" rel="ManyToOneRel">2</field>
+        <field type="CharField" name="name">Chinese Dinner</field>
+    </object>
+    <object pk="4" model="admin_views.restaurant">
+        <field to="admin_views.city" name="city" rel="ManyToOneRel">2</field>
+        <field type="CharField" name="name">Angels</field>
+    </object>
+    <object pk="5" model="admin_views.restaurant">
+        <field to="admin_views.city" name="city" rel="ManyToOneRel">2</field>
+        <field type="CharField" name="name">Take Away</field>
+    </object>
+    <object pk="6" model="admin_views.restaurant">
+        <field to="admin_views.city" name="city" rel="ManyToOneRel">3</field>
+        <field type="CharField" name="name">The Unknown Restaurant</field>
+    </object>
+    <object pk="1" model="admin_views.worker">
+        <field to="admin_views.restaurant" name="work_at" rel="ManyToOneRel">1</field>
+        <field type="CharField" name="name">Mario</field>
+        <field type="CharField" name="surname">Rossi</field>
+    </object>
+    <object pk="2" model="admin_views.worker">
+        <field to="admin_views.restaurant" name="work_at" rel="ManyToOneRel">1</field>
+        <field type="CharField" name="name">Antonio</field>
+        <field type="CharField" name="surname">Bianchi</field>
+    </object>
+    <object pk="3" model="admin_views.worker">
+        <field to="admin_views.restaurant" name="work_at" rel="ManyToOneRel">1</field>
+        <field type="CharField" name="name">John</field>
+        <field type="CharField" name="surname">Doe</field>
+    </object>
+</django-objects>
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index 050823f..8b001d8 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -20,7 +20,7 @@ class Article(models.Model):
 
     def __unicode__(self):
         return self.title
-    
+
     def model_year(self):
         return self.date.year
     model_year.admin_order_field = 'date'
@@ -79,6 +79,7 @@ class ChapterInline(admin.TabularInline):
 class ArticleAdmin(admin.ModelAdmin):
     list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year')
     list_filter = ('date',)
+    view_on_site = False
 
     def changelist_view(self, request):
         "Test that extra_context works"
@@ -134,12 +135,48 @@ class Thing(models.Model):
 class ThingAdmin(admin.ModelAdmin):
     list_filter = ('color',)
 
+class City(models.Model):
+    name = models.CharField(max_length=100)
+
+class Restaurant(models.Model):
+    city = models.ForeignKey(City)
+    name = models.CharField(max_length=100)
+
+class Worker(models.Model):
+    work_at = models.ForeignKey(Restaurant)
+    name = models.CharField(max_length=50)
+    surname = models.CharField(max_length=50)
+
+class RestaurantInlineAdmin(admin.TabularInline):
+    model = Restaurant
+    view_on_site = True
+
+class CityAdmin(admin.ModelAdmin):
+    inlines = [ RestaurantInlineAdmin ]
+    view_on_site = True
+
+class WorkerAdmin(admin.ModelAdmin):
+    def view_on_site(self, obj):
+        return '/worker/%s/%s/' % (obj.surname, obj.name)
+
+class WorkerInlineAdmin(admin.TabularInline):
+    model = Worker
+    def view_on_site(self, obj):
+        return '/worker_inline/%s/%s/' % (obj.surname, obj.name)
+
+class RestaurantAdmin(admin.ModelAdmin):
+    inlines = [ WorkerInlineAdmin ]
+    view_on_site = False
+
 admin.site.register(Article, ArticleAdmin)
 admin.site.register(CustomArticle, CustomArticleAdmin)
 admin.site.register(Section, inlines=[ArticleInline])
 admin.site.register(ModelWithStringPrimaryKey)
 admin.site.register(Color)
 admin.site.register(Thing, ThingAdmin)
+admin.site.register(City, CityAdmin)
+admin.site.register(Restaurant, RestaurantAdmin)
+admin.site.register(Worker, WorkerAdmin)
 
 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
 # That way we cover all four cases:
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index 391d1ff..1663c34 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -9,7 +9,7 @@ from django.contrib.admin.util import quote
 from django.utils.html import escape
 
 # local test models
-from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
+from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, City, Restaurant, Worker
 
 class AdminViewBasicTest(TestCase):
     fixtures = ['admin-views-users.xml', 'admin-views-colors.xml']
@@ -735,3 +735,63 @@ class AdminViewUnicodeTest(TestCase):
         self.failUnlessEqual(response.status_code, 200)
         response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
         self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
+
+class AdminViewOnSiteTest(TestCase):
+    fixtures = ['admin-views-users.xml', 'admin-views-restaurants.xml']
+    
+    def setUp(self):
+        self.client.login(username='super', password='secret')
+    
+    def tearDown(self):
+        self.client.logout()
+
+    def test_false(self):
+        "Ensure that the 'View on site' button is not displayed if view_on_site is False"
+        response = self.client.get('/test_admin/admin/admin_views/restaurant/1/')
+        content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
+        self.failIf(
+            '"../../../r/%s/1/"' % content_type_pk in response.content,
+            '"View on site" button displayed even if it has been disabled.')
+
+    def test_true(self):
+        "Ensure that the default behaviour is followed if view_on_site is True"
+        response = self.client.get('/test_admin/admin/admin_views/city/1/')
+        content_type_pk = ContentType.objects.get_for_model(City).pk
+        self.failUnless(
+            '"../../../r/%s/1/"' % content_type_pk in response.content,
+            '"View on site" is enabled but has not been displayed.')
+
+    def test_callable(self):
+        "Ensure that the right link is displayed if view_on_site is a callable"
+        response = self.client.get('/test_admin/admin/admin_views/worker/1/')
+        worker = Worker.objects.get(pk=1)
+        self.failUnless(
+            '"/worker/%s/%s/"' % (worker.surname, worker.name) in response.content,
+            '"View on site" is defined but has not been displayed.')
+
+class InlineAdminViewOnSiteTest(TestCase):
+    fixtures = ['admin-views-users.xml', 'admin-views-restaurants.xml']
+    
+    def setUp(self):
+        self.client.login(username='super', password='secret')
+    
+    def tearDown(self):
+        self.client.logout()
+
+    def test_true(self):
+        "Ensure that the 'View on site' button is displayed if view_on_site is True"
+        response = self.client.get('/test_admin/admin/admin_views/city/1/')
+        content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
+        self.failUnless(
+            '../../../r/%s/1/' % content_type_pk in response.content,
+            '"View on site" is enabled but has not been displayed.')
+
+    def test_callable(self):
+        "Ensure that the default behaviour is followed if view_on_site is True"
+        response = self.client.get('/test_admin/admin/admin_views/restaurant/1/')
+        content_type_pk = ContentType.objects.get_for_model(Worker).pk
+        worker = Worker.objects.get(pk=1)
+        self.failUnless(
+            '"/worker_inline/%s/%s/"' % (worker.surname, worker.name) in response.content,
+            '"View on site" is defined but has not been displayed.')
+
