Merge pull request #670 from tomchristie/generic-form-input

PUT or PATCH raw data (eg. json) from Browseable API
This commit is contained in:
Tom Christie 2013-02-22 00:42:37 -08:00
commit ef3303eb37
8 changed files with 175 additions and 51 deletions

View File

@ -115,7 +115,7 @@ The TemplateHTMLRenderer will create a `RequestContext`, using the `response.dat
The template name is determined by (in order of preference): The template name is determined by (in order of preference):
1. An explicit `.template_name` attribute set on the response. 1. An explicit `template_name` argument passed to the response.
2. An explicit `.template_name` attribute set on this class. 2. An explicit `.template_name` attribute set on this class.
3. The return result of calling `view.get_template_names()`. 3. The return result of calling `view.get_template_names()`.

View File

@ -345,12 +345,11 @@ class BrowsableAPIRenderer(BaseRenderer):
if not self.show_form_for_method(view, method, request, obj): if not self.show_form_for_method(view, method, request, obj):
return return
if method == 'DELETE' or method == 'OPTIONS': if method in ('DELETE', 'OPTIONS'):
return True # Don't actually need to return a form return True # Don't actually need to return a form
if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes:
media_types = [parser.media_type for parser in view.parser_classes] return
return self.get_generic_content_form(media_types)
serializer = view.get_serializer(instance=obj) serializer = view.get_serializer(instance=obj)
fields = self.serializer_to_form_fields(serializer) fields = self.serializer_to_form_fields(serializer)
@ -362,7 +361,7 @@ class BrowsableAPIRenderer(BaseRenderer):
form_instance = OnTheFlyForm(data) form_instance = OnTheFlyForm(data)
return form_instance return form_instance
def get_generic_content_form(self, media_types): def get_raw_data_form(self, view, method, request, media_types):
""" """
Returns a form that allows for arbitrary content types to be tunneled Returns a form that allows for arbitrary content types to be tunneled
via standard HTML forms. via standard HTML forms.
@ -375,6 +374,11 @@ class BrowsableAPIRenderer(BaseRenderer):
and api_settings.FORM_CONTENTTYPE_OVERRIDE): and api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None return None
# Check permissions
obj = getattr(view, 'object', None)
if not self.show_form_for_method(view, method, request, obj):
return
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
content_field = api_settings.FORM_CONTENT_OVERRIDE content_field = api_settings.FORM_CONTENT_OVERRIDE
choices = [(media_type, media_type) for media_type in media_types] choices = [(media_type, media_type) for media_type in media_types]
@ -386,7 +390,7 @@ class BrowsableAPIRenderer(BaseRenderer):
super(GenericContentForm, self).__init__() super(GenericContentForm, self).__init__()
self.fields[content_type_field] = forms.ChoiceField( self.fields[content_type_field] = forms.ChoiceField(
label='Content Type', label='Media type',
choices=choices, choices=choices,
initial=initial initial=initial
) )
@ -422,15 +426,22 @@ class BrowsableAPIRenderer(BaseRenderer):
view = renderer_context['view'] view = renderer_context['view']
request = renderer_context['request'] request = renderer_context['request']
response = renderer_context['response'] response = renderer_context['response']
media_types = [parser.media_type for parser in view.parser_classes]
renderer = self.get_default_renderer(view) renderer = self.get_default_renderer(view)
content = self.get_content(renderer, data, accepted_media_type, renderer_context) content = self.get_content(renderer, data, accepted_media_type, renderer_context)
put_form = self.get_form(view, 'PUT', request) put_form = self.get_form(view, 'PUT', request)
post_form = self.get_form(view, 'POST', request) post_form = self.get_form(view, 'POST', request)
patch_form = self.get_form(view, 'PATCH', request)
delete_form = self.get_form(view, 'DELETE', request) delete_form = self.get_form(view, 'DELETE', request)
options_form = self.get_form(view, 'OPTIONS', request) options_form = self.get_form(view, 'OPTIONS', request)
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request, media_types)
raw_data_post_form = self.get_raw_data_form(view, 'POST', request, media_types)
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request, media_types)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
name = self.get_name(view) name = self.get_name(view)
description = self.get_description(view) description = self.get_description(view)
breadcrumb_list = get_breadcrumbs(request.path) breadcrumb_list = get_breadcrumbs(request.path)
@ -447,10 +458,18 @@ class BrowsableAPIRenderer(BaseRenderer):
'breadcrumblist': breadcrumb_list, 'breadcrumblist': breadcrumb_list,
'allowed_methods': view.allowed_methods, 'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in view.renderer_classes], 'available_formats': [renderer.format for renderer in view.renderer_classes],
'put_form': put_form, 'put_form': put_form,
'post_form': post_form, 'post_form': post_form,
'patch_form': patch_form,
'delete_form': delete_form, 'delete_form': delete_form,
'options_form': options_form, 'options_form': options_form,
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
'raw_data_patch_form': raw_data_patch_form,
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
'api_settings': api_settings 'api_settings': api_settings
}) })

View File

@ -150,6 +150,48 @@ html, body {
margin: 0 auto -60px; margin: 0 auto -60px;
} }
.form-switcher {
margin-bottom: 0;
}
.well .form-actions {
padding-bottom: 0;
margin-bottom: 0;
}
.well form {
margin-bottom: 0;
}
.nav-tabs {
border: 0;
}
.nav-tabs > li {
margin-bottom: -3px;
float: right;
}
.nav-tabs li a {
margin-right: 0;
}
.nav-tabs > .active > a {
background: #f5f5f5;
}
.nav-tabs > .active > a:hover {
background: #f5f5f5;
}
.tabs-below > .nav-tabs {
border-bottom: none !important;
}
.tabs-below > .nav-tabs > li {
margin-bottom: -2px !important;
margin-right: 0 !important;
}
#footer, #push { #footer, #push {
height: 60px; /* .push must be the same height as .footer */ height: 60px; /* .push must be the same height as .footer */

View File

@ -3,3 +3,5 @@ prettyPrint();
$('.js-tooltip').tooltip({ $('.js-tooltip').tooltip({
delay: 1000 delay: 1000
}); });
$('.form-switcher a:first').tab('show');

View File

@ -123,56 +123,88 @@
{% if response.status_code != 403 %} {% if response.status_code != 403 %}
{% if post_form %} {% if post_form or raw_data_post_form %}
<div class="well"> <div {% if post_form %}class="tabbable"{% endif %}>
<form action="{{ request.get_full_path }}" method="POST" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal"> {% if post_form %}
<fieldset> <ul class="nav nav-tabs form-switcher">
{% csrf_token %} <li><a href="#object-form" data-toggle="tab">HTML form</a></li>
{{ post_form.non_field_errors }} <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li>
{% for field in post_form %} </ul>
<div class="control-group"> <!--{% if field.errors %}error{% endif %}--> {% endif %}
{{ field.label_tag|add_class:"control-label" }} <div class="well tab-content">
<div class="controls"> {% if post_form %}
{{ field }} <div class="tab-pane" id="object-form">
<span class="help-inline">{{ field.help_text }}</span> {% with form=post_form %}
<!--{{ field.errors|add_class:"help-block" }}--> <form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
<fieldset>
{% include "rest_framework/form.html" %}
<div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
</div> </div>
</div> </fieldset>
{% endfor %} </form>
<div class="form-actions"> {% endwith %}
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> </div>
</div> {% endif %}
</fieldset> <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form">
</form> {% with form=raw_data_post_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset>
{% include "rest_framework/form.html" %}
<div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
</div>
</fieldset>
</form>
{% endwith %}
</div>
</div>
</div> </div>
{% endif %} {% endif %}
{% if put_form %} {% if put_form or raw_data_put_form or raw_data_patch_form %}
<div class="well"> <div {% if put_form %}class="tabbable"{% endif %}>
<form action="{{ request.get_full_path }}" method="POST" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal"> {% if put_form %}
<fieldset> <ul class="nav nav-tabs form-switcher">
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" /> <li><a href="#object-form" data-toggle="tab">HTML form</a></li>
{% csrf_token %} <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li>
{{ put_form.non_field_errors }} </ul>
{% for field in put_form %} {% endif %}
<div class="control-group"> <!--{% if field.errors %}error{% endif %}--> <div class="well tab-content">
{{ field.label_tag|add_class:"control-label" }} {% if put_form %}
<div class="controls"> <div class="tab-pane" id="object-form">
{{ field }} {% with form=put_form %}
<span class='help-inline'>{{ field.help_text }}</span> <form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
<!--{{ field.errors|add_class:"help-block" }}--> <fieldset>
{% include "rest_framework/form.html" %}
<div class="form-actions">
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
</div> </div>
</div> </fieldset>
{% endfor %} </form>
<div class="form-actions"> {% endwith %}
<button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button> </div>
</div> {% endif %}
<div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
</fieldset> {% with form=raw_data_put_or_patch_form %}
</form> <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset>
{% include "rest_framework/form.html" %}
<div class="form-actions">
{% if raw_data_put_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
{% endif %}
{% if raw_data_patch_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PUT request on the {{ name }} resource">PATCH</button>
{% endif %}
</div>
</fieldset>
</form>
{% endwith %}
</div>
</div>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -0,0 +1,13 @@
{% load rest_framework %}
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
{{ field }}
<span class="help-inline">{{ field.help_text }}</span>
<!--{{ field.errors|add_class:"help-block" }}-->
</div>
</div>
{% endfor %}

View File

@ -112,6 +112,9 @@ class POSTDeniedView(APIView):
def put(self, request): def put(self, request):
return Response() return Response()
def patch(self, request):
return Response()
class DocumentingRendererTests(TestCase): class DocumentingRendererTests(TestCase):
def test_only_permitted_forms_are_displayed(self): def test_only_permitted_forms_are_displayed(self):
@ -120,6 +123,7 @@ class DocumentingRendererTests(TestCase):
response = view(request).render() response = view(request).render()
self.assertNotContains(response, '>POST<') self.assertNotContains(response, '>POST<')
self.assertContains(response, '>PUT<') self.assertContains(response, '>PUT<')
self.assertContains(response, '>PATCH<')
class RendererEndToEndTests(TestCase): class RendererEndToEndTests(TestCase):

View File

@ -1,10 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.test.client import RequestFactory, FakePayload from django.test.client import FakePayload, Client as _Client, RequestFactory as _RequestFactory
from django.test.client import MULTIPART_CONTENT from django.test.client import MULTIPART_CONTENT
from rest_framework.compat import urlparse from rest_framework.compat import urlparse
class RequestFactory(RequestFactory): class RequestFactory(_RequestFactory):
def __init__(self, **defaults): def __init__(self, **defaults):
super(RequestFactory, self).__init__(**defaults) super(RequestFactory, self).__init__(**defaults)
@ -26,3 +26,15 @@ class RequestFactory(RequestFactory):
} }
r.update(extra) r.update(extra)
return self.request(**r) return self.request(**r)
class Client(_Client, RequestFactory):
def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
follow=False, **extra):
"""
Send a resource to the server using PATCH.
"""
response = super(Client, self).patch(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response