mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +03:00
Merge pull request #2926 from tomchristie/admin-style
Admin style renderer
This commit is contained in:
commit
1f55bc747b
|
@ -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`
|
||||
|
||||
**.template**: `'rest_framework/form.html'`
|
||||
|
||||
## BrowsableAPIRenderer
|
||||
|
||||
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.
|
||||
Renders data into HTML for the Browsable API:
|
||||
|
||||
![The BrowsableAPIRenderer](../img/quickstart.png)
|
||||
|
||||
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`
|
||||
|
||||
|
@ -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].
|
||||
|
|
BIN
docs/img/admin.png
Normal file
BIN
docs/img/admin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ body{
|
|||
}
|
||||
|
||||
#main-content h3, #main-content h4, #main-content h5 {
|
||||
font-weight: 500;
|
||||
font-weight: 300;
|
||||
margin-top: 15px
|
||||
}
|
||||
|
||||
|
|
|
@ -158,6 +158,9 @@ class BasePagination(object):
|
|||
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):
|
||||
"""
|
||||
|
@ -261,7 +264,7 @@ class PageNumberPagination(BasePagination):
|
|||
)
|
||||
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
|
||||
|
||||
|
|
|
@ -20,6 +20,20 @@ from rest_framework.reverse import reverse
|
|||
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
|
||||
|
@ -235,6 +249,9 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
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:
|
||||
|
@ -293,7 +310,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
|
||||
# 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 '
|
||||
|
@ -310,6 +327,12 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
)
|
||||
raise ImproperlyConfigured(msg % self.view_name)
|
||||
|
||||
if url is None:
|
||||
return None
|
||||
|
||||
name = self.get_name(value)
|
||||
return Hyperlink(url, name)
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(HyperlinkedRelatedField):
|
||||
"""
|
||||
|
|
|
@ -593,7 +593,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
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):
|
||||
"""
|
||||
|
@ -675,6 +675,90 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
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'
|
||||
|
|
|
@ -8,6 +8,30 @@ from django.core.urlresolvers import NoReverseMatch
|
|||
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):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
content running up underneath it. */
|
||||
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
232
rest_framework/templates/rest_framework/admin.html
Normal file
232
rest_framework/templates/rest_framework/admin.html
Normal file
|
@ -0,0 +1,232 @@
|
|||
{% load url from future %}
|
||||
{% load staticfiles %}
|
||||
{% load rest_framework %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
|
||||
{% block meta %}
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
{% endblock %}
|
||||
|
||||
<title>{% block title %}Django REST framework{% endblock %}</title>
|
||||
|
||||
{% block style %}
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
{% block body %}
|
||||
<body class="{% block bodyclass %}{% endblock %}">
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
{% block navbar %}
|
||||
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||
<div class="container">
|
||||
<span>
|
||||
{% block branding %}
|
||||
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
|
||||
Django REST framework <span class="version">{{ version }}</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
</span>
|
||||
<ul class="nav navbar-nav pull-right">
|
||||
{% block userlinks %}
|
||||
{% if user.is_authenticated %}
|
||||
{% optional_logout request user %}
|
||||
{% else %}
|
||||
{% optional_login request %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<div class="container">
|
||||
{% block breadcrumbs %}
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||
{% if forloop.last %}
|
||||
<li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Content -->
|
||||
<div id="content">
|
||||
|
||||
{% if 'GET' in allowed_methods %}
|
||||
<form id="get-form" class="pull-right">
|
||||
<fieldset>
|
||||
<div class="btn-group format-selection">
|
||||
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
|
||||
Format <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for format in available_formats %}
|
||||
<li>
|
||||
<a class="format-option"
|
||||
href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}'
|
||||
rel="nofollow">
|
||||
{{ format }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if post_form %}
|
||||
<button type="button" class="button-form btn btn-primary" data-toggle="modal" data-target="#createModal">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if put_form %}
|
||||
<button type="button" class="button-form btn btn-primary" data-toggle="modal" data-target="#editModal">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if delete_form %}
|
||||
<form class="button-form" action="{{ request.get_full_path }}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
||||
<button class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="content-main">
|
||||
<div class="page-header">
|
||||
<h1>{{ name }}</h1>
|
||||
</div>
|
||||
<div style="float:left">
|
||||
{% block description %}
|
||||
{{ description }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if paginator %}
|
||||
<nav style="float: right">
|
||||
{% get_pagination_html paginator %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div class="request-info" style="clear: both" >
|
||||
{% if style == 'list' %}
|
||||
{% include "rest_framework/admin/list.html" %}
|
||||
{% else %}
|
||||
{% include "rest_framework/admin/detail.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if paginator %}
|
||||
<nav style="float: right">
|
||||
{% get_pagination_html paginator %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- END Content -->
|
||||
</div><!-- /.container -->
|
||||
</div><!-- ./wrapper -->
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">Create</h4>
|
||||
</div>
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{ post_form }}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">Edit</h4>
|
||||
</div>
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{ put_form }}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error_form %}
|
||||
<!-- Errors Modal -->
|
||||
<div class="modal" id="errorModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">{{ error_title }}</h4>
|
||||
</div>
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{ error_form }}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="{{ request.method }}" type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
{% endblock %}
|
||||
</html>
|
10
rest_framework/templates/rest_framework/admin/detail.html
Normal file
10
rest_framework/templates/rest_framework/admin/detail.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% load rest_framework %}
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
{% for key, value in results.items %}
|
||||
{% if key in details %}
|
||||
<tr><th>{{ key|capfirst }}</th><td {{ value|add_nested_class }}>{{ value|format_value }}</td></tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
21
rest_framework/templates/rest_framework/admin/list.html
Normal file
21
rest_framework/templates/rest_framework/admin/list.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% load rest_framework %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>{% for column in columns%}<th>{{ column|capfirst }}</th>{% endfor %}<th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in results %}
|
||||
<tr>
|
||||
{% for key, value in row.items %}
|
||||
{% if key in columns %}
|
||||
<td {{ value|add_nested_class }} >
|
||||
{{ value|format_value }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<td><a href="{{ row.url }}"><span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
|
||||
</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,11 @@
|
|||
{% load rest_framework %}
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
{% for item in value %}
|
||||
<tr>
|
||||
<th>{{ forloop.counter0 }}</th>
|
||||
<td>{{ item|format_value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,2 @@
|
|||
{% load rest_framework %}
|
||||
{% for item in value %}{% if not forloop.first%},{% endif %} {{item|format_value}}{% endfor %}
|
|
@ -4,6 +4,7 @@ import re
|
|||
|
||||
from django import template
|
||||
from django.core.urlresolvers import NoReverseMatch, reverse
|
||||
from django.template import Context, loader
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text, iri_to_uri
|
||||
from django.utils.html import escape, smart_urlquote
|
||||
|
@ -106,6 +107,45 @@ def add_class(value, css_class):
|
|||
return value
|
||||
|
||||
|
||||
@register.filter
|
||||
def format_value(value):
|
||||
if getattr(value, 'is_hyperlink', False):
|
||||
return mark_safe('<a href=%s>%s</a>' % (value, escape(value.name)))
|
||||
if value in (True, False, None):
|
||||
return mark_safe('<code>%s</code>' % {True: 'true', False: 'false', None: 'null'}[value])
|
||||
elif isinstance(value, list):
|
||||
if any([isinstance(item, (list, dict)) for item in value]):
|
||||
template = loader.get_template('rest_framework/admin/list_value.html')
|
||||
else:
|
||||
template = loader.get_template('rest_framework/admin/simple_list_value.html')
|
||||
context = Context({'value': value})
|
||||
return template.render(context)
|
||||
elif isinstance(value, dict):
|
||||
template = loader.get_template('rest_framework/admin/dict_value.html')
|
||||
context = Context({'value': value})
|
||||
return template.render(context)
|
||||
elif isinstance(value, six.string_types):
|
||||
if (
|
||||
(value.startswith('http:') or value.startswith('https:')) and not
|
||||
re.search(r'\s', value)
|
||||
):
|
||||
return mark_safe('<a href="{value}">{value}</a>'.format(value=escape(value)))
|
||||
elif '@' in value and not re.search(r'\s', value):
|
||||
return mark_safe('<a href="mailto:{value}">{value}</a>'.format(value=escape(value)))
|
||||
elif '\n' in value:
|
||||
return mark_safe('<pre>%s</pre>' % escape(value))
|
||||
return six.text_type(value)
|
||||
|
||||
|
||||
@register.filter
|
||||
def add_nested_class(value):
|
||||
if isinstance(value, dict):
|
||||
return 'class=nested'
|
||||
if isinstance(value, list) and any([isinstance(item, (list, dict)) for item in value]):
|
||||
return 'class=nested'
|
||||
return ''
|
||||
|
||||
|
||||
# Bunch of stuff cloned from urlize
|
||||
TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "']", "'}", "'"]
|
||||
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'),
|
||||
|
|
|
@ -3,12 +3,12 @@ from __future__ import unicode_literals
|
|||
from django.core.urlresolvers import get_script_prefix, resolve
|
||||
|
||||
|
||||
def get_breadcrumbs(url):
|
||||
def get_breadcrumbs(url, request=None):
|
||||
"""
|
||||
Given a url returns a list of breadcrumbs, which are each a
|
||||
tuple of (name, url).
|
||||
"""
|
||||
|
||||
from rest_framework.reverse import preserve_builtin_query_params
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -34,7 +34,8 @@ def get_breadcrumbs(url):
|
|||
if not seen or seen[-1] != view:
|
||||
suffix = getattr(view, 'suffix', None)
|
||||
name = view_name_func(cls, suffix)
|
||||
breadcrumbs_list.insert(0, (name, prefix + url))
|
||||
insert_url = preserve_builtin_query_params(prefix + url, request)
|
||||
breadcrumbs_list.insert(0, (name, insert_url))
|
||||
seen.append(view)
|
||||
|
||||
if url == '':
|
||||
|
|
Loading…
Reference in New Issue
Block a user