{% if inline_admin_form.original or inline_admin_form.show_url %}
{% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
- {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %}
+ {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %}
{% endif %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 84f8b67e4d..0023d13f3b 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -164,6 +164,11 @@ class BaseValidator(object):
for idx, f in enumerate(val):
get_field(cls, model, "prepopulated_fields['%s'][%d]" % (field, idx), f)
+ def validate_view_on_site_url(self, cls, model):
+ 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 validate_ordering(self, cls, model):
" Validate that ordering refers to existing fields or is random. "
# ordering = None
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 8873375174..1301437df2 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -1091,6 +1091,37 @@ subclass::
:meth:`ModelAdmin.get_search_results` to provide additional or alternate
search behavior.
+.. attribute:: ModelAdmin.view_on_site
+
+ .. versionadded:: 1.7
+
+ Set ``view_on_site`` to control whether or not to display the "View on site" link.
+ This link should bring you to a URL where you can display the saved object.
+
+ This value can be either a boolean flag or a callable. If ``True`` (the
+ default), the object's :meth:`~django.db.models.Model.get_absolute_url`
+ method will be used to generate the url.
+
+ If your model has a :meth:`~django.db.models.Model.get_absolute_url` method
+ but you don't want the "View on site" button to appear, you only need to set
+ ``view_on_site`` to ``False``::
+
+ from django.contrib import admin
+
+ class PersonAdmin(admin.ModelAdmin):
+ view_on_site = False
+
+ In case it is a callable, it accepts the model instance as a parameter.
+ For example::
+
+ from django.contrib import admin
+ from django.core.urlresolvers import reverse
+
+ class PersonAdmin(admin.ModelAdmin):
+ def view_on_site(self, obj):
+ return 'http://example.com' + reverse('person-detail',
+ kwargs={'slug': obj.slug})
+
Custom template options
~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index 09014980d5..13db17cbd4 100644
--- a/docs/releases/1.7.txt
+++ b/docs/releases/1.7.txt
@@ -168,6 +168,10 @@ Minor features
` ``= None`` to disable
links on the change list page grid.
+* You may now specify :attr:`ModelAdmin.view_on_site
+ ` to control whether or not to
+ display the "View on site" link.
+
:mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 7dd6c91e71..36b35aeefe 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -31,7 +31,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture,
AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated,
RelatedPrepopulated, UndeletableObject, UnchangeableObject, UserMessenger, Simple, Choice,
ShortMessage, Telegram, FilteredManager, EmptyModelHidden,
- EmptyModelVisible, EmptyModelMixin)
+ EmptyModelVisible, EmptyModelMixin, State, City, Restaurant, Worker)
def callable_year(dt_value):
@@ -74,6 +74,7 @@ class ChapterXtra1Admin(admin.ModelAdmin):
class ArticleAdmin(admin.ModelAdmin):
list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year')
list_filter = ('date', 'section')
+ view_on_site = False
fieldsets = (
('Some fields', {
'classes': ('collapse',),
@@ -735,6 +736,35 @@ class EmptyModelMixinAdmin(admin.ModelAdmin):
form = FormWithVisibleAndHiddenField
fieldsets = EmptyModelVisibleAdmin.fieldsets
+class CityInlineAdmin(admin.TabularInline):
+ model = City
+ view_on_site = False
+
+class StateAdmin(admin.ModelAdmin):
+ inlines = [CityInlineAdmin]
+
+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
+
site = admin.AdminSite(name="admin")
site.register(Article, ArticleAdmin)
site.register(CustomArticle, CustomArticleAdmin)
@@ -785,6 +815,10 @@ site.register(MainPrepopulated, MainPrepopulatedAdmin)
site.register(UnorderedObject, UnorderedObjectAdmin)
site.register(UndeletableObject, UndeletableObjectAdmin)
site.register(UnchangeableObject, UnchangeableObjectAdmin)
+site.register(State, StateAdmin)
+site.register(City, CityAdmin)
+site.register(Restaurant, RestaurantAdmin)
+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/admin_views/fixtures/admin-views-restaurants.xml b/tests/admin_views/fixtures/admin-views-restaurants.xml
new file mode 100644
index 0000000000..81e67ee8d0
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-restaurants.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index bab144e6b5..67f3b4216a 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -717,3 +717,19 @@ class EmptyModelHidden(models.Model):
class EmptyModelMixin(models.Model):
""" See ticket #11277. """
+
+class State(models.Model):
+ name = models.CharField(max_length=100)
+
+class City(models.Model):
+ state = models.ForeignKey(State)
+ 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)
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 16fea943f9..d5232fb8f2 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -9,6 +9,7 @@ import unittest
from django.conf import settings, global_settings
from django.core import mail
from django.core.files import temp as tempfile
+from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, NoReverseMatch
# Register auth models with the admin.
from django.contrib.auth import get_permission_codename
@@ -48,8 +49,8 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount,
AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable,
Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject,
Simple, UndeletableObject, UnchangeableObject, Choice, ShortMessage,
- Telegram, Pizza, Topping, FilteredManager)
-from .admin import site, site2
+ Telegram, Pizza, Topping, FilteredManager, City, Restaurant, Worker)
+from .admin import site, site2, CityAdmin
ERROR_MESSAGE = "Please enter the correct username and password \
@@ -4597,3 +4598,87 @@ class TestLabelVisibility(TestCase):
def assert_fieldline_hidden(self, response):
self.assertContains(response, '