Fixed #13163 -- Added ability to show change links on inline objects in admin.

Thanks DrMeers for the suggestion.
This commit is contained in:
Nick Sandford 2014-07-25 13:07:04 +01:00 committed by Tim Graham
parent 9a922dcad1
commit 9d9f0acd7e
12 changed files with 81 additions and 13 deletions

View File

@ -1721,6 +1721,7 @@ class InlineModelAdmin(BaseModelAdmin):
verbose_name = None verbose_name = None
verbose_name_plural = None verbose_name_plural = None
can_delete = True can_delete = True
show_change_link = False
checks_class = InlineModelAdminChecks checks_class = InlineModelAdminChecks
@ -1728,6 +1729,7 @@ class InlineModelAdmin(BaseModelAdmin):
self.admin_site = admin_site self.admin_site = admin_site
self.parent_model = parent_model self.parent_model = parent_model
self.opts = self.model._meta self.opts = self.model._meta
self.has_registered_model = admin_site.is_registered(self.model)
super(InlineModelAdmin, self).__init__() super(InlineModelAdmin, self).__init__()
if self.verbose_name is None: if self.verbose_name is None:
self.verbose_name = self.model._meta.verbose_name self.verbose_name = self.model._meta.verbose_name

View File

@ -114,6 +114,12 @@ class AdminSite(object):
raise NotRegistered('The model %s is not registered' % model.__name__) raise NotRegistered('The model %s is not registered' % model.__name__)
del self._registry[model] del self._registry[model]
def is_registered(self, model):
"""
Check if a model class is registered with this `AdminSite`.
"""
return model in self._registry
def add_action(self, action, name=None): def add_action(self, action, name=None):
""" """
Register an action to be available globally. Register an action to be available globally.

View File

@ -632,7 +632,7 @@ div.breadcrumbs {
background: url(../img/icon_addlink.gif) 0 .2em no-repeat; background: url(../img/icon_addlink.gif) 0 .2em no-repeat;
} }
.changelink { .changelink, .inlinechangelink {
padding-left: 12px; padding-left: 12px;
background: url(../img/icon_changelink.gif) 0 .2em no-repeat; background: url(../img/icon_changelink.gif) 0 .2em no-repeat;
} }

View File

@ -1,11 +1,12 @@
{% load i18n admin_static %} {% load i18n admin_urls admin_static %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"> <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2> <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }} {{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> {% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% 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> <h3><b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% 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 %} {% 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> </h3>

View File

@ -1,4 +1,4 @@
{% load i18n admin_static admin_modify %} {% load i18n admin_urls admin_static admin_modify %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"> <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}"> <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}
@ -26,7 +26,10 @@
id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<td class="original"> <td class="original">
{% if inline_admin_form.original or inline_admin_form.show_url %}<p> {% 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.original %}
{{ inline_admin_form.original }}
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
{% endif %}
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% 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 %} </p>{% endif %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}

View File

@ -2025,6 +2025,13 @@ The ``InlineModelAdmin`` class adds:
Specifies whether or not inline objects can be deleted in the inline. Specifies whether or not inline objects can be deleted in the inline.
Defaults to ``True``. Defaults to ``True``.
.. attribute:: InlineModelAdmin.show_change_link
.. versionadded:: 1.8
Specifies whether or not inline objects that can be changed in the
admin have a link to the change form. Defaults to ``False``.
.. method:: InlineModelAdmin.get_formset(request, obj=None, **kwargs) .. method:: InlineModelAdmin.get_formset(request, obj=None, **kwargs)
Returns a :class:`~django.forms.models.BaseInlineFormSet` class for use in Returns a :class:`~django.forms.models.BaseInlineFormSet` class for use in

View File

@ -35,6 +35,10 @@ Minor features
:meth:`~django.contrib.admin.ModelAdmin.has_module_permission` :meth:`~django.contrib.admin.ModelAdmin.has_module_permission`
method to allow limiting access to the module on the admin index page. method to allow limiting access to the module on the admin index page.
* :class:`~django.contrib.admin.InlineModelAdmin` now has an attribute
:attr:`~django.contrib.admin.InlineModelAdmin.show_change_link` that
supports showing a link to an inline object's change form.
:mod:`django.contrib.auth` :mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -90,10 +90,12 @@ class TitleInline(admin.TabularInline):
class Inner4StackedInline(admin.StackedInline): class Inner4StackedInline(admin.StackedInline):
model = Inner4Stacked model = Inner4Stacked
show_change_link = True
class Inner4TabularInline(admin.TabularInline): class Inner4TabularInline(admin.TabularInline):
model = Inner4Tabular model = Inner4Tabular
show_change_link = True
class Holder4Admin(admin.ModelAdmin): class Holder4Admin(admin.ModelAdmin):
@ -212,3 +214,4 @@ site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2In
site.register(BinaryTree, inlines=[BinaryTreeAdmin]) site.register(BinaryTree, inlines=[BinaryTreeAdmin])
site.register(ExtraTerrestrial, inlines=[SightingInline]) site.register(ExtraTerrestrial, inlines=[SightingInline])
site.register(SomeParentModel, inlines=[SomeChildModelInline]) site.register(SomeParentModel, inlines=[SomeChildModelInline])
site.register([Question, Inner4Stacked, Inner4Tabular])

View File

@ -13,7 +13,9 @@ from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
Sighting, Novel, Chapter, FootNote, BinaryTree, SomeParentModel, Sighting, Novel, Chapter, FootNote, BinaryTree, SomeParentModel,
SomeChildModel) SomeChildModel, Poll, Question, Inner4Stacked, Inner4Tabular, Holder4)
INLINE_CHANGELINK_HTML = 'class="inlinechangelink">Change</a>'
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
@ -311,6 +313,38 @@ class TestInline(TestCase):
count=1 count=1
) )
def test_inlines_show_change_link_registered(self):
"Inlines `show_change_link` for registered models when enabled."
holder = Holder4.objects.create(dummy=1)
item1 = Inner4Stacked.objects.create(dummy=1, holder=holder)
item2 = Inner4Tabular.objects.create(dummy=1, holder=holder)
items = (
('inner4stacked', item1.pk),
('inner4tabular', item2.pk),
)
response = self.client.get('/admin/admin_inlines/holder4/%s/' % holder.pk)
self.assertTrue(response.context['inline_admin_formset'].opts.has_registered_model)
for model, pk in items:
url = '/admin/admin_inlines/%s/%s/' % (model, pk)
self.assertContains(response, '<a href="%s" %s' % (url, INLINE_CHANGELINK_HTML))
def test_inlines_show_change_link_unregistered(self):
"Inlines `show_change_link` disabled for unregistered models."
parent = ParentModelWithCustomPk.objects.create(my_own_pk="foo", name="Foo")
ChildModel1.objects.create(my_own_pk="bar", name="Bar", parent=parent)
ChildModel2.objects.create(my_own_pk="baz", name="Baz", parent=parent)
response = self.client.get('/admin/admin_inlines/parentmodelwithcustompk/foo/')
self.assertFalse(response.context['inline_admin_formset'].opts.has_registered_model)
self.assertNotContains(response, INLINE_CHANGELINK_HTML)
def test_tabular_inline_show_change_link_false_registered(self):
"Inlines `show_change_link` disabled by default."
poll = Poll.objects.create(name="New poll")
Question.objects.create(poll=poll)
response = self.client.get('/admin/admin_inlines/poll/%s/' % poll.pk)
self.assertTrue(response.context['inline_admin_formset'].opts.has_registered_model)
self.assertNotContains(response, INLINE_CHANGELINK_HTML)
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
ROOT_URLCONF="admin_inlines.urls") ROOT_URLCONF="admin_inlines.urls")

View File

@ -44,7 +44,7 @@ class TestAdminOrdering(TestCase):
The default ordering should be by name, as specified in the inner Meta The default ordering should be by name, as specified in the inner Meta
class. class.
""" """
ma = ModelAdmin(Band, None) ma = ModelAdmin(Band, admin.site)
names = [b.name for b in ma.get_queryset(request)] names = [b.name for b in ma.get_queryset(request)]
self.assertListEqual(['Aerosmith', 'Radiohead', 'Van Halen'], names) self.assertListEqual(['Aerosmith', 'Radiohead', 'Van Halen'], names)
@ -55,7 +55,7 @@ class TestAdminOrdering(TestCase):
""" """
class BandAdmin(ModelAdmin): class BandAdmin(ModelAdmin):
ordering = ('rank',) # default ordering is ('name',) ordering = ('rank',) # default ordering is ('name',)
ma = BandAdmin(Band, None) ma = BandAdmin(Band, admin.site)
names = [b.name for b in ma.get_queryset(request)] names = [b.name for b in ma.get_queryset(request)]
self.assertListEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names) self.assertListEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names)
@ -67,7 +67,7 @@ class TestAdminOrdering(TestCase):
other_user = User.objects.create(username='other') other_user = User.objects.create(username='other')
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = super_user request.user = super_user
ma = DynOrderingBandAdmin(Band, None) ma = DynOrderingBandAdmin(Band, admin.site)
names = [b.name for b in ma.get_queryset(request)] names = [b.name for b in ma.get_queryset(request)]
self.assertListEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names) self.assertListEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names)
request.user = other_user request.user = other_user
@ -94,7 +94,7 @@ class TestInlineModelAdminOrdering(TestCase):
The default ordering should be by name, as specified in the inner Meta The default ordering should be by name, as specified in the inner Meta
class. class.
""" """
inline = SongInlineDefaultOrdering(self.band, None) inline = SongInlineDefaultOrdering(self.band, admin.site)
names = [s.name for s in inline.get_queryset(request)] names = [s.name for s in inline.get_queryset(request)]
self.assertListEqual(['Dude (Looks Like a Lady)', 'Jaded', 'Pink'], names) self.assertListEqual(['Dude (Looks Like a Lady)', 'Jaded', 'Pink'], names)
@ -102,7 +102,7 @@ class TestInlineModelAdminOrdering(TestCase):
""" """
Let's check with ordering set to something different than the default. Let's check with ordering set to something different than the default.
""" """
inline = SongInlineNewOrdering(self.band, None) inline = SongInlineNewOrdering(self.band, admin.site)
names = [s.name for s in inline.get_queryset(request)] names = [s.name for s in inline.get_queryset(request)]
self.assertListEqual(['Jaded', 'Pink', 'Dude (Looks Like a Lady)'], names) self.assertListEqual(['Jaded', 'Pink', 'Dude (Looks Like a Lady)'], names)

View File

@ -70,6 +70,15 @@ class TestRegistration(TestCase):
""" """
self.assertRaises(ImproperlyConfigured, self.site.register, Location) self.assertRaises(ImproperlyConfigured, self.site.register, Location)
def test_is_registered_model(self):
"Checks for registered models should return true."
self.site.register(Person)
self.assertTrue(self.site.is_registered(Person))
def test_is_registered_not_registered_model(self):
"Checks for unregistered models should return false."
self.assertFalse(self.site.is_registered(Person))
class TestRegistrationDecorator(TestCase): class TestRegistrationDecorator(TestCase):
""" """

View File

@ -256,8 +256,7 @@ class GenericInlineAdminWithUniqueTogetherTest(TestCase):
class NoInlineDeletionTest(TestCase): class NoInlineDeletionTest(TestCase):
def test_no_deletion(self): def test_no_deletion(self):
fake_site = object() inline = MediaPermanentInline(EpisodePermanent, admin_site)
inline = MediaPermanentInline(EpisodePermanent, fake_site)
fake_request = object() fake_request = object()
formset = inline.get_formset(fake_request) formset = inline.get_formset(fake_request)
self.assertFalse(formset.can_delete) self.assertFalse(formset.can_delete)