mirror of https://github.com/django/django.git
Fixed #13163 -- Added ability to show change links on inline objects in admin.
Thanks DrMeers for the suggestion.
This commit is contained in:
parent
9a922dcad1
commit
9d9f0acd7e
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> <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> <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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue