Repository: encode/django-rest-framework

Issue:
AdminRenderer does not correctly link to detail page if Serializer is not HyperlinkedModelSerializer
If the `Serializer` in the view is `ModelSerializer` rather than a `HyperlinkedModelSerializer`, the arrow link to the detail page does not work (it just points to the list page).

![screen shot 2015-08-27 at 15 56 35](https://cloud.githubusercontent.com/assets/647359/9523952/506e03f4-4cd4-11e5-9487-1c34ec8a86bd.png)



Discussion:
Milestoning to consider. May well be that we _only_ support the admin fully with hyperlinks, since we're need them when rendering the interface.

I'm going to de-milestone for now. We can reassess after 3.7

Resolving patch:
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -18,6 +18,7 @@
 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
 
@@ -808,6 +809,12 @@ def get_context(self, data, accepted_media_type, renderer_context):
         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
@@ -816,6 +823,26 @@ def get_context(self, data, accepted_media_type, renderer_context):
         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'


Test patch:
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -708,6 +708,75 @@ def get(self, request):
         response.render()
         self.assertContains(response, '<tr><th>Iteritems</th><td>a string</td></tr>', 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):