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.
This commit is contained in:
Ryan P Kilby 2018-07-06 04:58:26 -04:00 committed by Carlton Gibson
parent 3578bd6883
commit f89cc066bc
3 changed files with 101 additions and 1 deletions

View File

@ -18,6 +18,7 @@ from django.core.paginator import Page
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.template import engines, loader from django.template import engines, loader
from django.test.client import encode_multipart from django.test.client import encode_multipart
from django.urls import NoReverseMatch
from django.utils import six from django.utils import six
from django.utils.html import mark_safe from django.utils.html import mark_safe
@ -815,6 +816,12 @@ class AdminRenderer(BrowsableAPIRenderer):
columns = [key for key in header if key != 'url'] columns = [key for key in header if key != 'url']
details = [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['style'] = style
context['columns'] = columns context['columns'] = columns
context['details'] = details context['details'] = details
@ -823,6 +830,26 @@ class AdminRenderer(BrowsableAPIRenderer):
context['error_title'] = getattr(self, 'error_title', None) context['error_title'] = getattr(self, 'error_title', None)
return context 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): class DocumentationRenderer(BaseRenderer):
media_type = 'text/html' media_type = 'text/html'

View File

@ -1,7 +1,7 @@
{% load rest_framework %} {% load rest_framework %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr>{% for column in columns%}<th>{{ column|capfirst }}</th>{% endfor %}<th></th></tr> <tr>{% for column in columns%}<th>{{ column|capfirst }}</th>{% endfor %}<th class="col-xs-1"></th></tr>
</thead> </thead>
<tbody> <tbody>
{% for row in results %} {% for row in results %}
@ -14,7 +14,11 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<td> <td>
{% if row.url %}
<a href="{{ row.url }}"><span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></a> <a href="{{ row.url }}"><span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></a>
{% else %}
<span class="glyphicon glyphicon-chevron-right text-muted" aria-hidden="true"></span>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -728,6 +728,75 @@ class AdminRendererTests(TestCase):
response.render() response.render()
self.assertContains(response, '<tr><th>Iteritems</th><td>a string</td></tr>', html=True) 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') @pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
class TestDocumentationRenderer(TestCase): class TestDocumentationRenderer(TestCase):