Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin style #2926

Merged
merged 18 commits into from
Jul 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 36 additions & 14 deletions docs/api-guide/renderers.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,13 @@ You can use `StaticHTMLRenderer` either to return regular HTML pages using REST

See also: `TemplateHTMLRenderer`

## HTMLFormRenderer

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.

**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.

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

**.format**: `'.form'`

**.charset**: `utf-8`
## BrowsableAPIRenderer

**.template**: `'rest_framework/form.html'`
Renders data into HTML for the Browsable API:

## BrowsableAPIRenderer
![The BrowsableAPIRenderer](../img/quickstart.png)

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.
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.

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

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

## AdminRenderer

Renders data into HTML for an admin-like display:

![The AdminRender view](../img/admin.png)

This renderer is suitable for CRUD-style web APIs that should also present a user-friendly interface for managing the data.

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.

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

**.format**: `'.admin'`

**.charset**: `utf-8`

**.template**: `'rest_framework/admin.html'`

## HTMLFormRenderer

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.

**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.

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

**.format**: `'.form'`

**.charset**: `utf-8`

**.template**: `'rest_framework/form.html'`

## MultiPartRenderer

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].
Expand Down
Binary file added docs/img/admin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/tutorial/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer


Expand Down
2 changes: 1 addition & 1 deletion docs_theme/css/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ body{
}

#main-content h3, #main-content h4, #main-content h5 {
font-weight: 500;
font-weight: 300;
margin-top: 15px
}

Expand Down
5 changes: 4 additions & 1 deletion rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ def get_paginated_response(self, data): # pragma: no cover
def to_html(self): # pragma: no cover
raise NotImplementedError('to_html() must be implemented to display page controls.')

def get_results(self, data):
return data['results']


class PageNumberPagination(BasePagination):
"""
Expand Down Expand Up @@ -261,7 +264,7 @@ def paginate_queryset(self, queryset, request, view=None):
)
raise NotFound(msg)

if paginator.count > 1 and self.template is not None:
if paginator.num_pages > 1 and self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True

Expand Down
25 changes: 24 additions & 1 deletion rest_framework/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@
from rest_framework.utils import html


class Hyperlink(six.text_type):
"""
A string like object that additionally has an associated name.
We use this for hyperlinked URLs that may render as a named link
in some contexts, or render as a plain URL in others.
"""
def __new__(self, url, name):
ret = six.text_type.__new__(self, url)
ret.name = name
return ret

is_hyperlink = True


class PKOnlyObject(object):
"""
This is a mock object, used for when we only need the pk of the object
Expand Down Expand Up @@ -228,6 +242,9 @@ def get_url(self, obj, view_name, request, format):
kwargs = {self.lookup_url_kwarg: lookup_value}
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)

def get_name(self, obj):
return six.text_type(obj)

def to_internal_value(self, data):
request = self.context.get('request', None)
try:
Expand Down Expand Up @@ -286,7 +303,7 @@ def to_representation(self, value):

# Return the hyperlink, or error if incorrectly configured.
try:
return self.get_url(value, self.view_name, request, format)
url = self.get_url(value, self.view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
Expand All @@ -303,6 +320,12 @@ def to_representation(self, value):
)
raise ImproperlyConfigured(msg % self.view_name)

if url is None:
return None

name = self.get_name(value)
return Hyperlink(url, name)


class HyperlinkedIdentityField(HyperlinkedRelatedField):
"""
Expand Down
86 changes: 85 additions & 1 deletion rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ def get_description(self, view):
return view.get_view_description(html=True)

def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path)
return get_breadcrumbs(request.path, request)

def get_context(self, data, accepted_media_type, renderer_context):
"""
Expand Down Expand Up @@ -675,6 +675,90 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
return ret


class AdminRenderer(BrowsableAPIRenderer):
template = 'rest_framework/admin.html'
format = 'admin'

def render(self, data, accepted_media_type=None, renderer_context=None):
self.accepted_media_type = accepted_media_type or ''
self.renderer_context = renderer_context or {}

response = renderer_context['response']
request = renderer_context['request']
view = self.renderer_context['view']

if response.status_code == status.HTTP_400_BAD_REQUEST:
# Errors still need to display the list or detail information.
# The only way we can get at that is to simulate a GET request.
self.error_form = self.get_rendered_html_form(data, view, request.method, request)
self.error_title = {'POST': 'Create', 'PUT': 'Edit'}.get(request.method, 'Errors')

with override_method(view, request, 'GET') as request:
response = view.get(request, *view.args, **view.kwargs)
data = response.data

template = loader.get_template(self.template)
context = self.get_context(data, accepted_media_type, renderer_context)
context = RequestContext(renderer_context['request'], context)
ret = template.render(context)

# Creation and deletion should use redirects in the admin style.
if (response.status_code == status.HTTP_201_CREATED) and ('Location' in response):
response.status_code = status.HTTP_302_FOUND
response['Location'] = request.build_absolute_uri()
ret = ''

if response.status_code == status.HTTP_204_NO_CONTENT:
response.status_code = status.HTTP_302_FOUND
try:
# Attempt to get the parent breadcrumb URL.
response['Location'] = self.get_breadcrumbs(request)[-2][1]
except KeyError:
# Otherwise reload current URL to get a 'Not Found' page.
response['Location'] = request.full_path
ret = ''

return ret

def get_context(self, data, accepted_media_type, renderer_context):
"""
Render the HTML for the browsable API representation.
"""
context = super(AdminRenderer, self).get_context(
data, accepted_media_type, renderer_context
)

paginator = getattr(context['view'], 'paginator', None)
if (paginator is not None and data is not None):
try:
results = paginator.get_results(data)
except KeyError:
results = data
else:
results = data

if results is None:
header = {}
style = 'detail'
elif isinstance(results, list):
header = results[0] if results else {}
style = 'list'
else:
header = results
style = 'detail'

columns = [key for key in header.keys() if key != 'url']
details = [key for key in header.keys() if key != 'url']

context['style'] = style
context['columns'] = columns
context['details'] = details
context['results'] = results
context['error_form'] = getattr(self, 'error_form', None)
context['error_title'] = getattr(self, 'error_title', None)
return context


class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart'
Expand Down
32 changes: 29 additions & 3 deletions rest_framework/reverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@
from django.utils import six
from django.utils.functional import lazy

from rest_framework.settings import api_settings
from rest_framework.utils.urls import replace_query_param


def preserve_builtin_query_params(url, request=None):
"""
Given an incoming request, and an outgoing URL representation,
append the value of any built-in query parameters.
"""
if request is None:
return url

overrides = [
api_settings.URL_FORMAT_OVERRIDE,
api_settings.URL_ACCEPT_OVERRIDE
]

for param in overrides:
if param and (param in request.GET):
value = request.GET[param]
url = replace_query_param(url, param, value)

return url


def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
Expand All @@ -18,13 +42,15 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
scheme = getattr(request, 'versioning_scheme', None)
if scheme is not None:
try:
return scheme.reverse(viewname, args, kwargs, request, format, **extra)
url = scheme.reverse(viewname, args, kwargs, request, format, **extra)
except NoReverseMatch:
# In case the versioning scheme reversal fails, fallback to the
# default implementation
pass
url = _reverse(viewname, args, kwargs, request, format, **extra)
else:
url = _reverse(viewname, args, kwargs, request, format, **extra)

return _reverse(viewname, args, kwargs, request, format, **extra)
return preserve_builtin_query_params(url, request)


def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
Expand Down
10 changes: 9 additions & 1 deletion rest_framework/static/rest_framework/css/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
content running up underneath it. */

h1 {
font-weight: 500;
font-weight: 300;
}

h2, h3 {
Expand Down Expand Up @@ -33,6 +33,14 @@ h2, h3 {
margin-right: 1em;
}

td.nested {
padding: 0 !important;
}

td.nested > table {
margin: 0;
}

form select, form input, form textarea {
width: 90%;
}
Expand Down
4 changes: 4 additions & 0 deletions rest_framework/static/rest_framework/js/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ if (selectedTab && selectedTab.length > 0) {
// If no tab selected, display rightmost tab.
$('.form-switcher a:first').tab('show');
}

$(window).load(function(){
$('#errorModal').modal('show');
});
Loading