diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index ca4844321..a9645cc89 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -18,6 +18,7 @@ from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import engines, loader
from django.test.client import encode_multipart
+from django.urls import NoReverseMatch
from django.utils import six
from django.utils.html import mark_safe
@@ -815,6 +816,12 @@ class AdminRenderer(BrowsableAPIRenderer):
columns = [key for key in header if key != 'url']
details = [key for key in header if key != 'url']
+ if isinstance(results, list) and 'view' in renderer_context:
+ for result in results:
+ url = self.get_result_url(result, context['view'])
+ if url is not None:
+ result.setdefault('url', url)
+
context['style'] = style
context['columns'] = columns
context['details'] = details
@@ -823,6 +830,26 @@ class AdminRenderer(BrowsableAPIRenderer):
context['error_title'] = getattr(self, 'error_title', None)
return context
+ def get_result_url(self, result, view):
+ """
+ Attempt to reverse the result's detail view URL.
+
+ This only works with views that are generic-like (has `.lookup_field`)
+ and viewset-like (has `.basename` / `.reverse_action()`).
+ """
+ if not hasattr(view, 'reverse_action') or \
+ not hasattr(view, 'lookup_field'):
+ return
+
+ lookup_field = view.lookup_field
+ lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or lookup_field
+
+ try:
+ kwargs = {lookup_url_kwarg: result[lookup_field]}
+ return view.reverse_action('detail', kwargs=kwargs)
+ except (KeyError, NoReverseMatch):
+ return
+
class DocumentationRenderer(BaseRenderer):
media_type = 'text/html'
diff --git a/rest_framework/templates/rest_framework/admin/list.html b/rest_framework/templates/rest_framework/admin/list.html
index fd394d44e..ab3e84d17 100644
--- a/rest_framework/templates/rest_framework/admin/list.html
+++ b/rest_framework/templates/rest_framework/admin/list.html
@@ -1,7 +1,7 @@
{% load rest_framework %}
- {% for column in columns%}{{ column|capfirst }} | {% endfor %} |
+ {% for column in columns%}{{ column|capfirst }} | {% endfor %} |
{% for row in results %}
@@ -14,7 +14,11 @@
{% endif %}
{% endfor %}
+ {% if row.url %}
+ {% else %}
+
+ {% endif %}
|
{% endfor %}
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 6d802b1a7..a68ece734 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -728,6 +728,75 @@ class AdminRendererTests(TestCase):
response.render()
self.assertContains(response, 'Iteritems | a string |
', html=True)
+ def test_get_result_url(self):
+ factory = APIRequestFactory()
+
+ class DummyGenericViewsetLike(APIView):
+ lookup_field = 'test'
+
+ def reverse_action(view, *args, **kwargs):
+ self.assertEqual(kwargs['kwargs']['test'], 1)
+ return '/example/'
+
+ # get the view instance instead of the view function
+ view = DummyGenericViewsetLike.as_view()
+ request = factory.get('/')
+ response = view(request)
+ view = response.renderer_context['view']
+
+ self.assertEqual(self.renderer.get_result_url({'test': 1}, view), '/example/')
+ self.assertIsNone(self.renderer.get_result_url({}, view))
+
+ def test_get_result_url_no_result(self):
+ factory = APIRequestFactory()
+
+ class DummyView(APIView):
+ lookup_field = 'test'
+
+ # get the view instance instead of the view function
+ view = DummyView.as_view()
+ request = factory.get('/')
+ response = view(request)
+ view = response.renderer_context['view']
+
+ self.assertIsNone(self.renderer.get_result_url({'test': 1}, view))
+ self.assertIsNone(self.renderer.get_result_url({}, view))
+
+ def test_get_context_result_urls(self):
+ factory = APIRequestFactory()
+
+ class DummyView(APIView):
+ lookup_field = 'test'
+
+ def reverse_action(view, url_name, args=None, kwargs=None):
+ return '/%s/%d' % (url_name, kwargs['test'])
+
+ # get the view instance instead of the view function
+ view = DummyView.as_view()
+ request = factory.get('/')
+ response = view(request)
+
+ data = [
+ {'test': 1},
+ {'url': '/example', 'test': 2},
+ {'url': None, 'test': 3},
+ {},
+ ]
+ context = {
+ 'view': DummyView(),
+ 'request': Request(request),
+ 'response': response
+ }
+
+ context = self.renderer.get_context(data, None, context)
+ results = context['results']
+
+ self.assertEqual(len(results), 4)
+ self.assertEqual(results[0]['url'], '/detail/1')
+ self.assertEqual(results[1]['url'], '/example')
+ self.assertEqual(results[2]['url'], None)
+ self.assertNotIn('url', results[3])
+
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
class TestDocumentationRenderer(TestCase):