Kaydet (Commit) 9d9f0acd authored tarafından Nick Sandford's avatar Nick Sandford Kaydeden (comit) Tim Graham

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

Thanks DrMeers for the suggestion.
üst 9a922dca
...@@ -1721,6 +1721,7 @@ class InlineModelAdmin(BaseModelAdmin): ...@@ -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): ...@@ -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): ...@@ -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 { ...@@ -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;
} }
......
{% 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>
......
{% 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 @@ ...@@ -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: ...@@ -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 ...@@ -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): ...@@ -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 ...@@ -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, ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment