summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Rebetez <julien@fhtagn.net>2020-01-18 19:03:03 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-10-08 19:53:49 +0200
commitb79088306513d5ed76d31ac40ab3c15f858946ea (patch)
treee384bad39afe191ff65fba937b0f4ff7a3a8c8cb
parent855fc06236630464055b4f9ea422c68a07c6d02a (diff)
Fixed #31181 -- Added links to related models for admin's readonly fields.
-rw-r--r--django/contrib/admin/helpers.py23
-rw-r--r--docs/releases/3.2.txt3
-rw-r--r--tests/admin_views/admin.py9
-rw-r--r--tests/admin_views/models.py9
-rw-r--r--tests/admin_views/tests.py54
5 files changed, 87 insertions, 11 deletions
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 56d7970d2a..6f0be6b148 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -3,12 +3,15 @@ import json
from django import forms
from django.contrib.admin.utils import (
display_for_field, flatten_fieldsets, help_text_for_field, label_for_field,
- lookup_field,
+ lookup_field, quote,
)
from django.core.exceptions import ObjectDoesNotExist
-from django.db.models import ManyToManyRel
+from django.db.models.fields.related import (
+ ForeignObjectRel, ManyToManyRel, OneToOneField,
+)
from django.forms.utils import flatatt
from django.template.defaultfilters import capfirst, linebreaksbr
+from django.urls import NoReverseMatch, reverse
from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext, gettext_lazy as _
@@ -187,6 +190,17 @@ class AdminReadonlyField:
label = self.field['label']
return format_html('<label{}>{}{}</label>', flatatt(attrs), capfirst(label), self.form.label_suffix)
+ def get_admin_url(self, remote_field, remote_obj):
+ url_name = 'admin:%s_%s_change' % (
+ remote_field.model._meta.app_label,
+ remote_field.model._meta.model_name,
+ )
+ try:
+ url = reverse(url_name, args=[quote(remote_obj.pk)])
+ return format_html('<a href="{}">{}</a>', url, remote_obj)
+ except NoReverseMatch:
+ return str(remote_obj)
+
def contents(self):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
@@ -212,6 +226,11 @@ class AdminReadonlyField:
else:
if isinstance(f.remote_field, ManyToManyRel) and value is not None:
result_repr = ", ".join(map(str, value.all()))
+ elif (
+ isinstance(f.remote_field, (ForeignObjectRel, OneToOneField)) and
+ value is not None
+ ):
+ result_repr = self.get_admin_url(f.remote_field, value)
else:
result_repr = display_for_field(value, f, self.empty_value_display)
result_repr = linebreaksbr(result_repr)
diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt
index d82c9c719a..ab1d4dfb9a 100644
--- a/docs/releases/3.2.txt
+++ b/docs/releases/3.2.txt
@@ -72,6 +72,9 @@ Minor features
* :attr:`.ModelAdmin.search_fields` now allows searching against quoted phrases
with spaces.
+* Read-only related fields are now rendered as navigable links if target models
+ are registered in the admin.
+
:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 44623e483f..4a72e3070f 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -37,8 +37,8 @@ from .models import (
Person, Persona, Picture, Pizza, Plot, PlotDetails, PlotProxy,
PluggableSearchPerson, Podcast, Post, PrePopulatedPost,
PrePopulatedPostLargeSlug, PrePopulatedSubPost, Promo, Question,
- ReadablePizza, ReadOnlyPizza, Recipe, Recommendation, Recommender,
- ReferencedByGenRel, ReferencedByInline, ReferencedByParent,
+ ReadablePizza, ReadOnlyPizza, ReadOnlyRelatedField, Recipe, Recommendation,
+ Recommender, ReferencedByGenRel, ReferencedByInline, ReferencedByParent,
RelatedPrepopulated, RelatedWithUUIDPKModel, Report, Reservation,
Restaurant, RowLevelChangePermissionModel, Section, ShortMessage, Simple,
Sketch, Song, State, Story, StumpJoke, Subscriber, SuperVillain, Telegram,
@@ -539,6 +539,10 @@ class PizzaAdmin(admin.ModelAdmin):
readonly_fields = ('toppings',)
+class ReadOnlyRelatedFieldAdmin(admin.ModelAdmin):
+ readonly_fields = ('chapter', 'language', 'user')
+
+
class StudentAdmin(admin.ModelAdmin):
search_fields = ('name',)
@@ -1061,6 +1065,7 @@ site.register(GenRelReference)
site.register(ParentWithUUIDPK)
site.register(RelatedPrepopulated, search_fields=['name'])
site.register(RelatedWithUUIDPKModel)
+site.register(ReadOnlyRelatedField, ReadOnlyRelatedFieldAdmin)
# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
# That way we cover all four cases:
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index ba818ea10e..eed3cae306 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -365,6 +365,9 @@ class Language(models.Model):
english_name = models.CharField(max_length=50)
shortlist = models.BooleanField(default=False)
+ def __str__(self):
+ return self.iso
+
class Meta:
ordering = ('iso',)
@@ -999,3 +1002,9 @@ class UserProxy(User):
"""Proxy a model with a different app_label."""
class Meta:
proxy = True
+
+
+class ReadOnlyRelatedField(models.Model):
+ chapter = models.ForeignKey(Chapter, models.CASCADE)
+ language = models.ForeignKey(Language, models.CASCADE)
+ user = models.ForeignKey(User, models.CASCADE)
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 6842b02d3e..94646f2034 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -48,12 +48,12 @@ from .models import (
Parent, ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona,
Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post,
PrePopulatedPost, Promo, Question, ReadablePizza, ReadOnlyPizza,
- Recommendation, Recommender, RelatedPrepopulated, RelatedWithUUIDPKModel,
- Report, Restaurant, RowLevelChangePermissionModel, SecretHideout, Section,
- ShortMessage, Simple, Song, State, Story, SuperSecretHideout, SuperVillain,
- Telegram, TitleTranslation, Topping, UnchangeableObject, UndeletableObject,
- UnorderedObject, UserProxy, Villain, Vodcast, Whatsit, Widget, Worker,
- WorkHour,
+ ReadOnlyRelatedField, Recommendation, Recommender, RelatedPrepopulated,
+ RelatedWithUUIDPKModel, Report, Restaurant, RowLevelChangePermissionModel,
+ SecretHideout, Section, ShortMessage, Simple, Song, State, Story,
+ SuperSecretHideout, SuperVillain, Telegram, TitleTranslation, Topping,
+ UnchangeableObject, UndeletableObject, UnorderedObject, UserProxy, Villain,
+ Vodcast, Whatsit, Widget, Worker, WorkHour,
)
ERROR_MESSAGE = "Please enter the correct username and password \
@@ -5042,6 +5042,45 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
response = self.client.get(reverse('admin:admin_views_choice_change', args=(choice.pk,)))
self.assertContains(response, '<div class="readonly">No opinion</div>', html=True)
+ def test_readonly_foreignkey_links(self):
+ """
+ ForeignKey readonly fields render as links if the target model is
+ registered in admin.
+ """
+ chapter = Chapter.objects.create(
+ title='Chapter 1',
+ content='content',
+ book=Book.objects.create(name='Book 1'),
+ )
+ language = Language.objects.create(iso='_40', name='Test')
+ obj = ReadOnlyRelatedField.objects.create(
+ chapter=chapter,
+ language=language,
+ user=self.superuser,
+ )
+ response = self.client.get(
+ reverse('admin:admin_views_readonlyrelatedfield_change', args=(obj.pk,)),
+ )
+ # Related ForeignKey object registered in admin.
+ user_url = reverse('admin:auth_user_change', args=(self.superuser.pk,))
+ self.assertContains(
+ response,
+ '<div class="readonly"><a href="%s">super</a></div>' % user_url,
+ html=True,
+ )
+ # Related ForeignKey with the string primary key registered in admin.
+ language_url = reverse(
+ 'admin:admin_views_language_change',
+ args=(quote(language.pk),),
+ )
+ self.assertContains(
+ response,
+ '<div class="readonly"><a href="%s">_40</a></div>' % language_url,
+ html=True,
+ )
+ # Related ForeignKey object not registered in admin.
+ self.assertContains(response, '<div class="readonly">Chapter 1</div>', html=True)
+
def test_readonly_manytomany_backwards_ref(self):
"""
Regression test for #16433 - backwards references for related objects
@@ -5071,7 +5110,8 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
response = self.client.get(reverse('admin:admin_views_plotproxy_change', args=(pl.pk,)))
field = self.get_admin_readonly_field(response, 'plotdetails')
- self.assertEqual(field.contents(), 'Brand New Plot')
+ pd_url = reverse('admin:admin_views_plotdetails_change', args=(pd.pk,))
+ self.assertEqual(field.contents(), '<a href="%s">Brand New Plot</a>' % pd_url)
# The reverse relation also works if the OneToOneField is null.
pd.plot = None