Skip to content

Commit

Permalink
Fixed django#13163 -- Added ability to show change links on inline ob…
Browse files Browse the repository at this point in the history
…jects in admin.

Thanks DrMeers for the suggestion.
  • Loading branch information
slurms authored and timgraham committed Jul 31, 2014
1 parent 9a922dc commit 9d9f0ac
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 13 deletions.
2 changes: 2 additions & 0 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1721,13 +1721,15 @@ class InlineModelAdmin(BaseModelAdmin):
verbose_name = None
verbose_name_plural = None
can_delete = True
show_change_link = False

checks_class = InlineModelAdminChecks

def __init__(self, parent_model, admin_site):
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
Expand Down
6 changes: 6 additions & 0 deletions django/contrib/admin/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ def unregister(self, model_or_iterable):
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.
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/static/admin/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions django/contrib/admin/templates/admin/edit_inline/stacked.html
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
7 changes: 5 additions & 2 deletions django/contrib/admin/templates/admin/edit_inline/tabular.html
Original file line number Diff line number Diff line change
@@ -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 }}
Expand Down Expand Up @@ -26,7 +26,10 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
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 %}
Expand Down
7 changes: 7 additions & 0 deletions docs/ref/contrib/admin/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.8.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`
^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
3 changes: 3 additions & 0 deletions tests/admin_inlines/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -212,3 +214,4 @@ class SomeChildModelInline(admin.TabularInline):
site.register(BinaryTree, inlines=[BinaryTreeAdmin])
site.register(ExtraTerrestrial, inlines=[SightingInline])
site.register(SomeParentModel, inlines=[SomeChildModelInline])
site.register([Question, Inner4Stacked, Inner4Tabular])
36 changes: 35 additions & 1 deletion tests/admin_inlines/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
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',),
Expand Down Expand Up @@ -311,6 +313,38 @@ def test_stacked_inline_edit_form_contains_has_original_class(self):
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")
Expand Down
10 changes: 5 additions & 5 deletions tests/admin_ordering/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_default_ordering(self):
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)

Expand All @@ -55,7 +55,7 @@ def test_specified_ordering(self):
"""
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)

Expand All @@ -67,7 +67,7 @@ def test_dynamic_ordering(self):
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
Expand All @@ -94,15 +94,15 @@ def test_default_ordering(self):
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)

def test_specified_ordering(self):
"""
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)

Expand Down
9 changes: 9 additions & 0 deletions tests/admin_registration/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ def test_abstract_model(self):
"""
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):
"""
Expand Down
3 changes: 1 addition & 2 deletions tests/generic_inline_admin/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,7 @@ def test_add(self):
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)
Expand Down

0 comments on commit 9d9f0ac

Please sign in to comment.