diff options
| author | Tom Christie | 2013-02-22 00:42:37 -0800 |
|---|---|---|
| committer | Tom Christie | 2013-02-22 00:42:37 -0800 |
| commit | ef3303eb3702f365aeecb8296592cffa93c4afc7 (patch) | |
| tree | bc6297fb5bb590558dd312d976eb5225eb2f9119 /rest_framework | |
| parent | 47a4f0863d08e4b839ea3bbd7308ecc0f995b7d9 (diff) | |
| parent | 31f3fa63b2758d1e16f6814f4ac18647412bdddb (diff) | |
| download | django-rest-framework-ef3303eb3702f365aeecb8296592cffa93c4afc7.tar.bz2 | |
Merge pull request #670 from tomchristie/generic-form-input
PUT or PATCH raw data (eg. json) from Browseable API
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/renderers.py | 29 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/default.css | 42 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/js/default.js | 2 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 118 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/form.html | 13 | ||||
| -rw-r--r-- | rest_framework/tests/renderers.py | 4 | ||||
| -rw-r--r-- | rest_framework/tests/utils.py | 16 |
7 files changed, 174 insertions, 50 deletions
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a6525404..637904c4 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -345,12 +345,11 @@ class BrowsableAPIRenderer(BaseRenderer): if not self.show_form_for_method(view, method, request, obj): return - if method == 'DELETE' or method == 'OPTIONS': + if method in ('DELETE', 'OPTIONS'): 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: - media_types = [parser.media_type for parser in view.parser_classes] - return self.get_generic_content_form(media_types) + return serializer = view.get_serializer(instance=obj) fields = self.serializer_to_form_fields(serializer) @@ -362,7 +361,7 @@ class BrowsableAPIRenderer(BaseRenderer): form_instance = OnTheFlyForm(data) 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 via standard HTML forms. @@ -375,6 +374,11 @@ class BrowsableAPIRenderer(BaseRenderer): and api_settings.FORM_CONTENTTYPE_OVERRIDE): 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_field = api_settings.FORM_CONTENT_OVERRIDE choices = [(media_type, media_type) for media_type in media_types] @@ -386,7 +390,7 @@ class BrowsableAPIRenderer(BaseRenderer): super(GenericContentForm, self).__init__() self.fields[content_type_field] = forms.ChoiceField( - label='Content Type', + label='Media type', choices=choices, initial=initial ) @@ -422,15 +426,22 @@ class BrowsableAPIRenderer(BaseRenderer): view = renderer_context['view'] request = renderer_context['request'] response = renderer_context['response'] + media_types = [parser.media_type for parser in view.parser_classes] renderer = self.get_default_renderer(view) content = self.get_content(renderer, data, accepted_media_type, renderer_context) put_form = self.get_form(view, 'PUT', 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) 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) description = self.get_description(view) breadcrumb_list = get_breadcrumbs(request.path) @@ -447,10 +458,18 @@ class BrowsableAPIRenderer(BaseRenderer): 'breadcrumblist': breadcrumb_list, 'allowed_methods': view.allowed_methods, 'available_formats': [renderer.format for renderer in view.renderer_classes], + 'put_form': put_form, 'post_form': post_form, + 'patch_form': patch_form, 'delete_form': delete_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 }) diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index b2e41b99..4e7cbc06 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -150,6 +150,48 @@ html, body { 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 { height: 60px; /* .push must be the same height as .footer */ diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index ecaccc0f..484a3bdf 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -3,3 +3,5 @@ prettyPrint(); $('.js-tooltip').tooltip({ delay: 1000 }); + +$('.form-switcher a:first').tab('show');
\ No newline at end of file diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 8d807574..44633f5a 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -123,56 +123,88 @@ {% if response.status_code != 403 %} - {% if post_form %} - <div class="well"> - <form action="{{ request.get_full_path }}" method="POST" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal"> - <fieldset> - {% csrf_token %} - {{ post_form.non_field_errors }} - {% for field in post_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" }}--> + {% if post_form or raw_data_post_form %} + <div {% if post_form %}class="tabbable"{% endif %}> + {% if post_form %} + <ul class="nav nav-tabs form-switcher"> + <li><a href="#object-form" data-toggle="tab">HTML form</a></li> + <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li> + </ul> + {% endif %} + <div class="well tab-content"> + {% if post_form %} + <div class="tab-pane" id="object-form"> + {% with form=post_form %} + <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> - {% endfor %} - <div class="form-actions"> - <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> - </div> - </fieldset> - </form> + </fieldset> + </form> + {% endwith %} + </div> + {% endif %} + <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-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> {% endif %} - {% if put_form %} - <div class="well"> - <form action="{{ request.get_full_path }}" method="POST" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal"> - <fieldset> - <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" /> - {% csrf_token %} - {{ put_form.non_field_errors }} - {% for field in put_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" }}--> + {% if put_form or raw_data_put_form or raw_data_patch_form %} + <div {% if put_form %}class="tabbable"{% endif %}> + {% if put_form %} + <ul class="nav nav-tabs form-switcher"> + <li><a href="#object-form" data-toggle="tab">HTML form</a></li> + <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li> + </ul> + {% endif %} + <div class="well tab-content"> + {% if put_form %} + <div class="tab-pane" id="object-form"> + {% with form=put_form %} + <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 js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> </div> - </div> - {% endfor %} - <div class="form-actions"> - <button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button> - </div> - - </fieldset> - </form> + </fieldset> + </form> + {% endwith %} + </div> + {% endif %} + <div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form"> + {% with form=raw_data_put_or_patch_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> {% endif %} - {% endif %} </div> diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html new file mode 100644 index 00000000..dc7acc70 --- /dev/null +++ b/rest_framework/templates/rest_framework/form.html @@ -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 %} diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index e3f45ce6..90ef1221 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -112,6 +112,9 @@ class POSTDeniedView(APIView): def put(self, request): return Response() + def patch(self, request): + return Response() + class DocumentingRendererTests(TestCase): def test_only_permitted_forms_are_displayed(self): @@ -120,6 +123,7 @@ class DocumentingRendererTests(TestCase): response = view(request).render() self.assertNotContains(response, '>POST<') self.assertContains(response, '>PUT<') + self.assertContains(response, '>PATCH<') class RendererEndToEndTests(TestCase): diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py index 224c4f9d..8c87917d 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,10 +1,10 @@ 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 rest_framework.compat import urlparse -class RequestFactory(RequestFactory): +class RequestFactory(_RequestFactory): def __init__(self, **defaults): super(RequestFactory, self).__init__(**defaults) @@ -26,3 +26,15 @@ class RequestFactory(RequestFactory): } r.update(extra) 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 |
