Permalink
Browse files

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.
  • Loading branch information...
1 parent 497930b commit fd219fa24c7911adab60e1f5e4fd3d7f9d82a969 @unaizalakain unaizalakain committed with charettes Oct 24, 2013
@@ -202,9 +202,10 @@ def __init__(self, inline, formset, fieldsets, prepopulated_fields=None,
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)
@@ -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 @@ def formfield_for_manytomany(self, db_field, request=None, **kwargs):
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,14 +985,16 @@ def render_change_form(self, request, context, add=False, change=False, form_url
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,
'has_add_permission': self.has_add_permission(request),
'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,
@@ -32,7 +32,7 @@
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
</li>
- {% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" 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%}
{% endblock %}
</ul>
{% endif %}{% endif %}
@@ -6,7 +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|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>
- {% if inline_admin_form.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% 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 %}
{% 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.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
@@ -27,7 +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.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% 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 %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
@@ -164,6 +164,11 @@ def validate_prepopulated_fields(self, cls, model):
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
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~
View
@@ -168,6 +168,10 @@ Minor features
<django.contrib.admin.ModelAdmin.list_display_links>` ``= None`` to disable
links on the change list page grid.
+* You may now specify :attr:`ModelAdmin.view_on_site
+ <django.contrib.admin.ModelAdmin.view_on_site>` to control whether or not to
+ display the "View on site" link.
+
:mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^
View
@@ -31,7 +31,7 @@
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 @@ class EmptyModelMixinAdmin(admin.ModelAdmin):
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:
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.state">
+ <field type="CharField" name="name">New York</field>
+ </object>
+ <object pk="2" model="admin_views.state">
+ <field type="CharField" name="name">Illinois</field>
+ </object>
+ <object pk="3" model="admin_views.state">
+ <field type="CharField" name="name">California</field>
+ </object>
+ <object pk="1" model="admin_views.city">
+ <field to="admin_views.state" name="state" rel="ManyToOneRel">1</field>
+ <field type="CharField" name="name">New York</field>
+ </object>
+ <object pk="2" model="admin_views.city">
+ <field to="admin_views.state" name="state" rel="ManyToOneRel">2</field>
+ <field type="CharField" name="name">Chicago</field>
+ </object>
+ <object pk="3" model="admin_views.city">
+ <field to="admin_views.state" name="state" rel="ManyToOneRel">3</field>
+ <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>
@@ -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)
Oops, something went wrong.

0 comments on commit fd219fa

Please sign in to comment.