From fd219fa24c7911adab60e1f5e4fd3d7f9d82a969 Mon Sep 17 00:00:00 2001 From: Unai Zalakain Date: Thu, 24 Oct 2013 17:28:09 +0200 Subject: [PATCH] Fixed #8261 -- ModelAdmin hook for customising the "show on site" button ``ModelAdmin.view_on_site`` defines wether to show a link to the object on the admin detail page. If ``True``, cleverness (i.e. ``Model.get_absolute_url``) is used to get the url. If it's a callable, the callable is called with the object as the only parameter. If ``False``, not link is displayed. With the aim of maitaining backwards compatibility, ``True`` is the default. --- django/contrib/admin/helpers.py | 8 +- django/contrib/admin/options.py | 18 +++- .../admin/templates/admin/change_form.html | 2 +- .../templates/admin/edit_inline/stacked.html | 2 +- .../templates/admin/edit_inline/tabular.html | 2 +- django/contrib/admin/validation.py | 5 ++ docs/ref/contrib/admin/index.txt | 31 +++++++ docs/releases/1.7.txt | 4 + tests/admin_views/admin.py | 36 +++++++- .../fixtures/admin-views-restaurants.xml | 63 +++++++++++++ tests/admin_views/models.py | 16 ++++ tests/admin_views/tests.py | 89 ++++++++++++++++++- 12 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 tests/admin_views/fixtures/admin-views-restaurants.xml diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 622664fc6b..5bc19872e9 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -202,9 +202,10 @@ class InlineAdminFormSet(object): def __iter__(self): for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): + view_on_site_url = self.opts.get_view_on_site_url(original) yield InlineAdminForm(self.formset, form, self.fieldsets, self.prepopulated_fields, original, self.readonly_fields, - model_admin=self.opts) + model_admin=self.opts, view_on_site_url=view_on_site_url) for form in self.formset.extra_forms: yield InlineAdminForm(self.formset, form, self.fieldsets, self.prepopulated_fields, None, self.readonly_fields, @@ -242,13 +243,14 @@ class InlineAdminForm(AdminForm): A wrapper around an inline form for use in the admin system. """ def __init__(self, formset, form, fieldsets, prepopulated_fields, original, - readonly_fields=None, model_admin=None): + readonly_fields=None, model_admin=None, view_on_site_url=None): self.formset = formset self.model_admin = model_admin 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, readonly_fields, model_admin) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index fe5b5e0edf..e3514dd82a 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -98,6 +98,7 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): formfield_overrides = {} readonly_fields = () ordering = None + view_on_site = True # validation validator_class = validation.BaseValidator @@ -243,6 +244,19 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): return db_field.formfield(**kwargs) + def get_view_on_site_url(self, obj=None): + if obj is None or not self.view_on_site: + return None + + if callable(self.view_on_site): + return self.view_on_site(obj) + elif self.view_on_site: + # use the ContentType lookup if view_on_site is True + return reverse('admin:view_on_site', kwargs={ + 'content_type_id': ContentType.objects.get_for_model(obj).pk, + 'object_id': obj.pk + }) + @property def declared_fieldsets(self): warnings.warn( @@ -971,6 +985,7 @@ class ModelAdmin(BaseModelAdmin): app_label = opts.app_label preserved_filters = self.get_preserved_filters(request) form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url) + view_on_site_url = self.get_view_on_site_url(obj) context.update({ 'add': add, 'change': change, @@ -978,7 +993,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': view_on_site_url is not None, + 'absolute_url': view_on_site_url, 'form_url': form_url, 'opts': opts, 'content_type_id': ContentType.objects.get_for_model(self.model).id, diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 1acfcd37db..79ea73933c 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -32,7 +32,7 @@ {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} {% trans "History" %} - {% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif%} + {% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif%} {% endblock %} {% endif %}{% endif %} diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html index 42f68f3f97..0fa81ef597 100644 --- a/django/contrib/admin/templates/admin/edit_inline/stacked.html +++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html @@ -6,7 +6,7 @@ {% for inline_admin_form in inline_admin_formset %}

    {{ inline_admin_formset.opts.verbose_name|capfirst }}: {% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %} - {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %} + {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %} {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}{% 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 94be6f89b9..2a3463280d 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -27,7 +27,7 @@ {% 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 @@ + + + + New York + + + Illinois + + + California + + + 1 + New York + + + 2 + Chicago + + + 3 + San Francisco + + + 1 + Italian Pizza + + + 1 + Boulevard + + + 2 + Chinese Dinner + + + 2 + Angels + + + 2 + Take Away + + + 3 + The Unknown Restaurant + + + 1 + Mario + Rossi + + + 1 + Antonio + Bianchi + + + 1 + John + Doe + + 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, '