Skip to content

Commit 1f55bc7

Browse files
committed
Merge pull request #2926 from tomchristie/admin-style
Admin style renderer
2 parents 1f50f08 + 79b825e commit 1f55bc7

File tree

18 files changed

+513
-26
lines changed

18 files changed

+513
-26
lines changed

docs/api-guide/renderers.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -153,23 +153,13 @@ You can use `StaticHTMLRenderer` either to return regular HTML pages using REST
153153

154154
See also: `TemplateHTMLRenderer`
155155

156-
## HTMLFormRenderer
157-
158-
Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `<form>` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages.
159-
160-
**Note**: The `HTMLFormRenderer` class is intended for internal use with the browsable API. It should not be considered a fully documented or stable API. The template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely.
161-
162-
**.media_type**: `text/html`
163-
164-
**.format**: `'.form'`
165-
166-
**.charset**: `utf-8`
156+
## BrowsableAPIRenderer
167157

168-
**.template**: `'rest_framework/form.html'`
158+
Renders data into HTML for the Browsable API:
169159

170-
## BrowsableAPIRenderer
160+
![The BrowsableAPIRenderer](../img/quickstart.png)
171161

172-
Renders data into HTML for the Browsable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page.
162+
This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page.
173163

174164
**.media_type**: `text/html`
175165

@@ -187,6 +177,38 @@ By default the response content will be rendered with the highest priority rende
187177
def get_default_renderer(self, view):
188178
return JSONRenderer()
189179

180+
## AdminRenderer
181+
182+
Renders data into HTML for an admin-like display:
183+
184+
![The AdminRender view](../img/admin.png)
185+
186+
This renderer is suitable for CRUD-style web APIs that should also present a user-friendly interface for managing the data.
187+
188+
Note that views that have nested or list serializers for their input won't work well with the `AdminRenderer`, as the HTML forms are unable to properly support them.
189+
190+
**.media_type**: `text/html`
191+
192+
**.format**: `'.admin'`
193+
194+
**.charset**: `utf-8`
195+
196+
**.template**: `'rest_framework/admin.html'`
197+
198+
## HTMLFormRenderer
199+
200+
Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `<form>` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages.
201+
202+
**Note**: The `HTMLFormRenderer` class is intended for internal use with the browsable API and admin interface. It should not be considered a fully documented or stable API. The template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely.
203+
204+
**.media_type**: `text/html`
205+
206+
**.format**: `'.form'`
207+
208+
**.charset**: `utf-8`
209+
210+
**.template**: `'rest_framework/form.html'`
211+
190212
## MultiPartRenderer
191213

192214
This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing].

docs/img/admin.png

54.6 KB
Loading

docs/tutorial/quickstart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a
6868
"""
6969
API endpoint that allows users to be viewed or edited.
7070
"""
71-
queryset = User.objects.all()
71+
queryset = User.objects.all().order_by('-date_joined')
7272
serializer_class = UserSerializer
7373

7474

docs_theme/css/default.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ body{
176176
}
177177

178178
#main-content h3, #main-content h4, #main-content h5 {
179-
font-weight: 500;
179+
font-weight: 300;
180180
margin-top: 15px
181181
}
182182

rest_framework/pagination.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ def get_paginated_response(self, data): # pragma: no cover
158158
def to_html(self): # pragma: no cover
159159
raise NotImplementedError('to_html() must be implemented to display page controls.')
160160

161+
def get_results(self, data):
162+
return data['results']
163+
161164

162165
class PageNumberPagination(BasePagination):
163166
"""
@@ -261,7 +264,7 @@ def paginate_queryset(self, queryset, request, view=None):
261264
)
262265
raise NotFound(msg)
263266

264-
if paginator.count > 1 and self.template is not None:
267+
if paginator.num_pages > 1 and self.template is not None:
265268
# The browsable API should display pagination controls.
266269
self.display_page_controls = True
267270

rest_framework/relations.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@
2020
from rest_framework.utils import html
2121

2222

23+
class Hyperlink(six.text_type):
24+
"""
25+
A string like object that additionally has an associated name.
26+
We use this for hyperlinked URLs that may render as a named link
27+
in some contexts, or render as a plain URL in others.
28+
"""
29+
def __new__(self, url, name):
30+
ret = six.text_type.__new__(self, url)
31+
ret.name = name
32+
return ret
33+
34+
is_hyperlink = True
35+
36+
2337
class PKOnlyObject(object):
2438
"""
2539
This is a mock object, used for when we only need the pk of the object
@@ -235,6 +249,9 @@ def get_url(self, obj, view_name, request, format):
235249
kwargs = {self.lookup_url_kwarg: lookup_value}
236250
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
237251

252+
def get_name(self, obj):
253+
return six.text_type(obj)
254+
238255
def to_internal_value(self, data):
239256
request = self.context.get('request', None)
240257
try:
@@ -293,7 +310,7 @@ def to_representation(self, value):
293310

294311
# Return the hyperlink, or error if incorrectly configured.
295312
try:
296-
return self.get_url(value, self.view_name, request, format)
313+
url = self.get_url(value, self.view_name, request, format)
297314
except NoReverseMatch:
298315
msg = (
299316
'Could not resolve URL for hyperlinked relationship using '
@@ -310,6 +327,12 @@ def to_representation(self, value):
310327
)
311328
raise ImproperlyConfigured(msg % self.view_name)
312329

330+
if url is None:
331+
return None
332+
333+
name = self.get_name(value)
334+
return Hyperlink(url, name)
335+
313336

314337
class HyperlinkedIdentityField(HyperlinkedRelatedField):
315338
"""

rest_framework/renderers.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ def get_description(self, view):
593593
return view.get_view_description(html=True)
594594

595595
def get_breadcrumbs(self, request):
596-
return get_breadcrumbs(request.path)
596+
return get_breadcrumbs(request.path, request)
597597

598598
def get_context(self, data, accepted_media_type, renderer_context):
599599
"""
@@ -675,6 +675,90 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
675675
return ret
676676

677677

678+
class AdminRenderer(BrowsableAPIRenderer):
679+
template = 'rest_framework/admin.html'
680+
format = 'admin'
681+
682+
def render(self, data, accepted_media_type=None, renderer_context=None):
683+
self.accepted_media_type = accepted_media_type or ''
684+
self.renderer_context = renderer_context or {}
685+
686+
response = renderer_context['response']
687+
request = renderer_context['request']
688+
view = self.renderer_context['view']
689+
690+
if response.status_code == status.HTTP_400_BAD_REQUEST:
691+
# Errors still need to display the list or detail information.
692+
# The only way we can get at that is to simulate a GET request.
693+
self.error_form = self.get_rendered_html_form(data, view, request.method, request)
694+
self.error_title = {'POST': 'Create', 'PUT': 'Edit'}.get(request.method, 'Errors')
695+
696+
with override_method(view, request, 'GET') as request:
697+
response = view.get(request, *view.args, **view.kwargs)
698+
data = response.data
699+
700+
template = loader.get_template(self.template)
701+
context = self.get_context(data, accepted_media_type, renderer_context)
702+
context = RequestContext(renderer_context['request'], context)
703+
ret = template.render(context)
704+
705+
# Creation and deletion should use redirects in the admin style.
706+
if (response.status_code == status.HTTP_201_CREATED) and ('Location' in response):
707+
response.status_code = status.HTTP_302_FOUND
708+
response['Location'] = request.build_absolute_uri()
709+
ret = ''
710+
711+
if response.status_code == status.HTTP_204_NO_CONTENT:
712+
response.status_code = status.HTTP_302_FOUND
713+
try:
714+
# Attempt to get the parent breadcrumb URL.
715+
response['Location'] = self.get_breadcrumbs(request)[-2][1]
716+
except KeyError:
717+
# Otherwise reload current URL to get a 'Not Found' page.
718+
response['Location'] = request.full_path
719+
ret = ''
720+
721+
return ret
722+
723+
def get_context(self, data, accepted_media_type, renderer_context):
724+
"""
725+
Render the HTML for the browsable API representation.
726+
"""
727+
context = super(AdminRenderer, self).get_context(
728+
data, accepted_media_type, renderer_context
729+
)
730+
731+
paginator = getattr(context['view'], 'paginator', None)
732+
if (paginator is not None and data is not None):
733+
try:
734+
results = paginator.get_results(data)
735+
except KeyError:
736+
results = data
737+
else:
738+
results = data
739+
740+
if results is None:
741+
header = {}
742+
style = 'detail'
743+
elif isinstance(results, list):
744+
header = results[0] if results else {}
745+
style = 'list'
746+
else:
747+
header = results
748+
style = 'detail'
749+
750+
columns = [key for key in header.keys() if key != 'url']
751+
details = [key for key in header.keys() if key != 'url']
752+
753+
context['style'] = style
754+
context['columns'] = columns
755+
context['details'] = details
756+
context['results'] = results
757+
context['error_form'] = getattr(self, 'error_form', None)
758+
context['error_title'] = getattr(self, 'error_title', None)
759+
return context
760+
761+
678762
class MultiPartRenderer(BaseRenderer):
679763
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
680764
format = 'multipart'

rest_framework/reverse.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,30 @@
88
from django.utils import six
99
from django.utils.functional import lazy
1010

11+
from rest_framework.settings import api_settings
12+
from rest_framework.utils.urls import replace_query_param
13+
14+
15+
def preserve_builtin_query_params(url, request=None):
16+
"""
17+
Given an incoming request, and an outgoing URL representation,
18+
append the value of any built-in query parameters.
19+
"""
20+
if request is None:
21+
return url
22+
23+
overrides = [
24+
api_settings.URL_FORMAT_OVERRIDE,
25+
api_settings.URL_ACCEPT_OVERRIDE
26+
]
27+
28+
for param in overrides:
29+
if param and (param in request.GET):
30+
value = request.GET[param]
31+
url = replace_query_param(url, param, value)
32+
33+
return url
34+
1135

1236
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
1337
"""
@@ -18,13 +42,15 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
1842
scheme = getattr(request, 'versioning_scheme', None)
1943
if scheme is not None:
2044
try:
21-
return scheme.reverse(viewname, args, kwargs, request, format, **extra)
45+
url = scheme.reverse(viewname, args, kwargs, request, format, **extra)
2246
except NoReverseMatch:
2347
# In case the versioning scheme reversal fails, fallback to the
2448
# default implementation
25-
pass
49+
url = _reverse(viewname, args, kwargs, request, format, **extra)
50+
else:
51+
url = _reverse(viewname, args, kwargs, request, format, **extra)
2652

27-
return _reverse(viewname, args, kwargs, request, format, **extra)
53+
return preserve_builtin_query_params(url, request)
2854

2955

3056
def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):

rest_framework/static/rest_framework/css/default.css

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
content running up underneath it. */
44

55
h1 {
6-
font-weight: 500;
6+
font-weight: 300;
77
}
88

99
h2, h3 {
@@ -33,6 +33,14 @@ h2, h3 {
3333
margin-right: 1em;
3434
}
3535

36+
td.nested {
37+
padding: 0 !important;
38+
}
39+
40+
td.nested > table {
41+
margin: 0;
42+
}
43+
3644
form select, form input, form textarea {
3745
width: 90%;
3846
}

rest_framework/static/rest_framework/js/default.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@ if (selectedTab && selectedTab.length > 0) {
5959
// If no tab selected, display rightmost tab.
6060
$('.form-switcher a:first').tab('show');
6161
}
62+
63+
$(window).load(function(){
64+
$('#errorModal').modal('show');
65+
});

0 commit comments

Comments
 (0)