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_plural = None
can_delete = True
show_change_link = False
checks_class = InlineModelAdminChecks
@ -1728,6 +1729,7 @@ class InlineModelAdmin(BaseModelAdmin):
self.admin_site = admin_site
self.parent_model = parent_model
self.opts = self.model._meta
self.has_registered_model = admin_site.is_registered(self.model)
super(InlineModelAdmin, self).__init__()
if self.verbose_name is None:
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__)
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):
"""
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;
}
.changelink {
.changelink, .inlinechangelink {
padding-left: 12px;
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">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ 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 %}">
<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_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>

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="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ 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 %}">
<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.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 %}
</p>{% 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.
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)
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`
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`
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -90,10 +90,12 @@ class TitleInline(admin.TabularInline):
class Inner4StackedInline(admin.StackedInline):
model = Inner4Stacked
show_change_link = True
class Inner4TabularInline(admin.TabularInline):
model = Inner4Tabular
show_change_link = True
class Holder4Admin(admin.ModelAdmin):
@ -212,3 +214,4 @@ site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2In
site.register(BinaryTree, inlines=[BinaryTreeAdmin])
site.register(ExtraTerrestrial, inlines=[SightingInline])
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,
ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
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',),
@ -311,6 +313,38 @@ class TestInline(TestCase):
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',),
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
class.
"""
ma = ModelAdmin(Band, None)
ma = ModelAdmin(Band, admin.site)
names = [b.name for b in ma.get_queryset(request)]
self.assertListEqual(['Aerosmith', 'Radiohead', 'Van Halen'], names)
@ -55,7 +55,7 @@ class TestAdminOrdering(TestCase):
"""
class BandAdmin(ModelAdmin):
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)]
self.assertListEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names)
@ -67,7 +67,7 @@ class TestAdminOrdering(TestCase):
other_user = User.objects.create(username='other')
request = self.request_factory.get('/')
request.user = super_user
ma = DynOrderingBandAdmin(Band, None)
ma = DynOrderingBandAdmin(Band, admin.site)
names = [b.name for b in ma.get_queryset(request)]
self.assertListEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names)
request.user = other_user
@ -94,7 +94,7 @@ class TestInlineModelAdminOrdering(TestCase):
The default ordering should be by name, as specified in the inner Meta
class.
"""
inline = SongInlineDefaultOrdering(self.band, None)
inline = SongInlineDefaultOrdering(self.band, admin.site)
names = [s.name for s in inline.get_queryset(request)]
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.
"""
inline = SongInlineNewOrdering(self.band, None)
inline = SongInlineNewOrdering(self.band, admin.site)
names = [s.name for s in inline.get_queryset(request)]
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)
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):
"""

View File

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