From c28a5d9a000b419842db4a32874336cd844c8f97 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Thu, 17 Jan 2013 15:16:31 +0600 Subject: [PATCH 1/3] Added PATCH form to the Browsable API. Fix #570 --- rest_framework/renderers.py | 2 ++ .../templates/rest_framework/base.html | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a34abaa0..80c3b43db 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -427,6 +427,7 @@ class BrowsableAPIRenderer(BaseRenderer): content = self.get_content(renderer, data, accepted_media_type, renderer_context) put_form = self.get_form(view, 'PUT', request) + patch_form = self.get_form(view, 'PATCH', request) post_form = self.get_form(view, 'POST', request) delete_form = self.get_form(view, 'DELETE', request) options_form = self.get_form(view, 'OPTIONS', request) @@ -448,6 +449,7 @@ class BrowsableAPIRenderer(BaseRenderer): 'allowed_methods': view.allowed_methods, 'available_formats': [renderer.format for renderer in view.renderer_classes], 'put_form': put_form, + 'patch_form': patch_form, 'post_form': post_form, 'delete_form': delete_form, 'options_form': options_form, diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 092bf2e47..8ab9c5857 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -173,6 +173,31 @@ {% endif %} + {% if patch_form %} +
+
+
+ + {% csrf_token %} + {{ patch_form.non_field_errors }} + {% for field in patch_form %} +
+ {{ field.label_tag|add_class:"control-label" }} +
+ {{ field }} + {{ field.help_text }} + +
+
+ {% endfor %} +
+ +
+ +
+
+
+ {% endif %} {% endif %} From a1c322c34616d6019c9fbb216be2076f8b324c46 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Thu, 17 Jan 2013 15:20:24 +0600 Subject: [PATCH 2/3] Added tests for PATCH form in the Browsable API * Added the `tests.utils.Client` (`django.test.client.Client` subclass with patch method support) * Added tests for PATCH form in the Browsable API. --- rest_framework/tests/authentication.py | 14 +++++++++++++- rest_framework/tests/renderers.py | 4 ++++ rest_framework/tests/utils.py | 16 ++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index e86041bc4..3713e690d 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,12 +1,13 @@ from django.contrib.auth.models import User from django.http import HttpResponse -from django.test import Client, TestCase +from django.test import TestCase from rest_framework import permissions from rest_framework.authtoken.models import Token from rest_framework.authentication import TokenAuthentication from rest_framework.compat import patterns from rest_framework.views import APIView +from rest_framework.tests.utils import Client import json import base64 @@ -18,6 +19,9 @@ class MockView(APIView): def post(self, request): return HttpResponse({'a': 1, 'b': 2, 'c': 3}) + def patch(self, request): + return HttpResponse({'a': 1, 'b': 2, 'c': 3}) + def put(self, request): return HttpResponse({'a': 1, 'b': 2, 'c': 3}) @@ -102,6 +106,14 @@ class SessionAuthTests(TestCase): response = self.non_csrf_client.put('/', {'example': 'example'}) self.assertEqual(response.status_code, 200) + def test_patch_form_session_auth_passing(self): + """ + Ensure PATCHting form over session authentication with logged in user and CSRF token passes. + """ + self.non_csrf_client.login(username=self.username, password=self.password) + response = self.non_csrf_client.patch('/', {'example': 'example'}) + self.assertEqual(response.status_code, 200) + def test_post_form_session_auth_failing(self): """ Ensure POSTing form over session authentication without logged in user fails. diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index c1b4e624b..02ef0a3ac 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -111,6 +111,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): @@ -119,6 +122,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 3906adb9a..0cff55234 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,9 +1,9 @@ -from django.test.client import RequestFactory, FakePayload +from django.test.client import Client as _Client, RequestFactory as _RequestFactory, FakePayload from django.test.client import MULTIPART_CONTENT from urlparse import urlparse -class RequestFactory(RequestFactory): +class RequestFactory(_RequestFactory): def __init__(self, **defaults): super(RequestFactory, self).__init__(**defaults) @@ -25,3 +25,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 From e53b66bed811e53a2430601ddb38eb78941dbdea Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 25 Jan 2013 15:18:22 +0600 Subject: [PATCH 3/3] Added tabs between PUT and PATCH forms and better PATCH form handing --- .../static/rest_framework/js/default.js | 8 ++ .../templates/rest_framework/base.html | 99 ++++++++++--------- 2 files changed, 61 insertions(+), 46 deletions(-) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index ecaccc0f0..fc386d6b8 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -3,3 +3,11 @@ prettyPrint(); $('.js-tooltip').tooltip({ delay: 1000 }); + +$('#patch-form').find('.field-switcher').on('change', function() { + var $this = $(this); + $('#patch-form').find('#'+$this.attr('data-field-id')) + .prop('disabled', !$this.prop('checked')); +}); + +$('#form-method-switcher a:first').tab('show'); diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 8ab9c5857..0c0b3351b 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -147,56 +147,63 @@ {% endif %} - {% if put_form %} + {% if put_form or patch_form %}
-
-
- - {% csrf_token %} - {{ put_form.non_field_errors }} - {% for field in put_form %} -
- {{ field.label_tag|add_class:"control-label" }} -
- {{ field }} - {{ field.help_text }} - -
-
- {% endfor %} -
- -
- -
-
-
- {% endif %} - - {% if patch_form %} -
-
-
- - {% csrf_token %} - {{ patch_form.non_field_errors }} - {% for field in patch_form %} -
- {{ field.label_tag|add_class:"control-label" }} -
- {{ field }} - {{ field.help_text }} - + +
+ {% if put_form %} +
+ +
+ + {% csrf_token %} + {{ put_form.non_field_errors }} + {% for field in put_form %} +
+ {{ field.label_tag|add_class:"control-label" }} +
+ {{ field }} + {{ field.help_text }} + +
+ {% endfor %} +
+
- {% endfor %} -
- -
- -
- +
+ +
+ {% endif %} + {% if patch_form %} +
+
+
+ + {% csrf_token %} + {{ patch_form.non_field_errors }} + {% for field in patch_form %} +
+ {{ field.label_tag|add_class:"control-label" }} +
+ {{ field }}  + {{ field.help_text }} + +
+
+ {% endfor %} +
+ +
+
+
+
+ {% endif %} + {% endif %} {% endif %}