From f89cc066bc8980c823488bd87f55fc51ab5c0599 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 6 Jul 2018 04:58:26 -0400 Subject: [PATCH] Admin renderer urls (#5988) * Make admin detail link have small width * Disable admin detail link when no URL * Add 'AdminRenderer.get_result_url' Attempts to reverse the result's detail view URL. --- rest_framework/renderers.py | 27 ++++++++ .../templates/rest_framework/admin/list.html | 6 +- tests/test_renderers.py | 69 +++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) 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%}{% endfor %} + {% for column in columns%}{% endfor %} {% for row in results %} @@ -14,7 +14,11 @@ {% endif %} {% endfor %} {% 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, '', 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):
{{ column|capfirst }}
{{ column|capfirst }}
+ {% if row.url %} + {% else %} + + {% endif %}
Iteritemsa string