From 8fdf9250157cde2341ec9c86ead44b2ed1354aa2 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 14:41:12 +0600 Subject: [PATCH 01/68] Added tabs between object form and generic content form on PUT/PATCH form Some extra behaviour to `BrowsableAPIRenderer` to handle PATCH form. Added PATCH button on generic content PUT form. Tabs between object form and generic content form on PUT/PATCH form wich are both allways visible now. Fix #570 Refs #591 --- rest_framework/renderers.py | 10 ++- .../static/rest_framework/js/default.js | 2 + .../templates/rest_framework/base.html | 63 ++++++++++++------- .../templates/rest_framework/form.html | 13 ++++ 4 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 rest_framework/templates/rest_framework/form.html diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a65254042..736384d64 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) @@ -422,14 +421,17 @@ 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) + generic_content_form = self.get_generic_content_form(media_types) name = self.get_name(view) description = self.get_description(view) @@ -449,8 +451,10 @@ class BrowsableAPIRenderer(BaseRenderer): '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, + 'generic_content_form': generic_content_form, 'api_settings': api_settings }) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index ecaccc0f0..bc5b02928 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 8d807574b..87e5dc04e 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -147,32 +147,49 @@ {% endif %} - {% if put_form %} + {% if 'PUT' in allowed_methods or 'PATCH' in allowed_methods %}
-
-
- - {% csrf_token %} - {{ put_form.non_field_errors }} - {% for field in put_form %} -
- {{ field.label_tag|add_class:"control-label" }} -
- {{ field }} - {{ field.help_text }} - -
-
- {% endfor %} -
- -
- -
-
+ +
+ {% if put_form %} +
+ {% with form=put_form %} +
+
+ {% include "rest_framework/form.html" %} +
+ +
+
+
+ {% endwith %} +
+ {% endif %} +
+ {% with form=generic_content_form %} +
+
+ {% include "rest_framework/form.html" %} +
+ {% if 'PUT' in allowed_methods %} + + {% endif %} + {% if 'PATCH' in allowed_methods %} + + {% endif %} +
+
+
+ {% endwith %} +
+
{% endif %} - {% endif %} diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html new file mode 100644 index 000000000..dc7acc708 --- /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 %} +
+ {{ field.label_tag|add_class:"control-label" }} +
+ {{ field }} + {{ field.help_text }} + +
+
+{% endfor %} From d3f6536365cefa01f93cfadcc5e6a737d5c5fa80 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 15:33:36 +0600 Subject: [PATCH 02/68] Added tests for PATCH form in the Browsable API --- rest_framework/tests/renderers.py | 4 ++++ rest_framework/tests/utils.py | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index e3f45ce60..90ef12212 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 224c4f9d3..8c87917d9 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 From 3195f72784a2d55d10f3d7a58acdfee694e89e4b Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 16:39:24 +0600 Subject: [PATCH 03/68] POST form using new form.html template --- .../templates/rest_framework/base.html | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 87e5dc04e..fb541e944 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,25 +125,16 @@ {% if post_form %}
-
-
- {% csrf_token %} - {{ post_form.non_field_errors }} - {% for field in post_form %} -
- {{ field.label_tag|add_class:"control-label" }} -
- {{ field }} - {{ field.help_text }} - -
+ {% with form=post_form %} + +
+ {% include "rest_framework/form.html" %} +
+
- {% endfor %} -
- -
-
- +
+ + {% endwith %}
{% endif %} From 533e47235210d735dbe68d96fb55460eca19be9b Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 18:25:36 +0600 Subject: [PATCH 04/68] Added tabs between object form and generic content form on POST form --- .../templates/rest_framework/base.html | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index fb541e944..9d47a2edd 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,16 +125,40 @@ {% if post_form %}
- {% with form=post_form %} -
-
- {% include "rest_framework/form.html" %} -
- -
-
-
- {% endwith %} + +
+ {% if post_form %} +
+ {% with form=post_form %} +
+
+ {% include "rest_framework/form.html" %} +
+ +
+
+
+ {% endwith %} +
+ {% endif %} +
+ {% with form=generic_content_form %} +
+
+ {% include "rest_framework/form.html" %} +
+ +
+
+
+ {% endwith %} +
+
{% endif %} From 66a6ffaf957405691d0714fc422b46a6927639a7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Feb 2013 17:09:28 +0000 Subject: [PATCH 05/68] Fix typos. --- docs/api-guide/relations.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 25fca4753..5a9d74b09 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -43,7 +43,7 @@ In order to explain the various types of relational fields, we'll use a couple o For example, the following serializer. - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = RelatedField(many=True) class Meta: @@ -75,7 +75,7 @@ This field is read only. For example, the following serializer: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = PrimaryKeyRelatedField(many=True, read_only=True) class Meta: @@ -109,7 +109,7 @@ By default this field is read-write, although you can change this behavior using For example, the following serializer: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = HyperlinkedRelatedField(many=True, read_only=True, view_name='track-detail') @@ -149,7 +149,7 @@ By default this field is read-write, although you can change this behavior using For example, the following serializer: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = SlugRelatedField(many=True, read_only=True, slug_field='title') class Meta: @@ -223,12 +223,12 @@ Note that nested relationships are currently read-only. For read-write relation For example, the following serializer: - class TrackSerializer(serializer.ModelSerializer): + class TrackSerializer(serializers.ModelSerializer): class Meta: model = Track fields = ('order', 'title') - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = TrackSerializer(many=True) class Meta: @@ -265,7 +265,7 @@ For, example, we could define a relational field, to serialize a track to a cust duration = time.strftime('%M:%S', time.gmtime(value.duration)) return 'Track %d: %s (%s)' % (value.order, value.name, duration) - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = TrackListingField(many=True) class Meta: @@ -295,13 +295,13 @@ Note that reverse relationships are not automatically generated by the `ModelSer **The following will not work:** - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): class Meta: fields = ('tracks', ...) Instead, you must explicitly add it to the serializer. For example: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = serializers.PrimaryKeyRelationship(many=True) ... @@ -315,7 +315,7 @@ The best way to ensure this is typically to make sure that the relationship on t Alternatively, you can use the `source` argument on the serializer field, to use a different accessor attribute than the field name. For example. - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = serializers.PrimaryKeyRelationship(many=True, source='track_set') See the Django documentation on [reverse relationships][reverse-relationships] for more details. From c5cf51cf511c84ab3e446376ff38170dcd421958 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Feb 2013 17:16:48 +0000 Subject: [PATCH 06/68] Fix typos. --- docs/api-guide/relations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 5a9d74b09..623fe1a90 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -302,7 +302,7 @@ Note that reverse relationships are not automatically generated by the `ModelSer Instead, you must explicitly add it to the serializer. For example: class AlbumSerializer(serializers.ModelSerializer): - tracks = serializers.PrimaryKeyRelationship(many=True) + tracks = serializers.PrimaryKeyRelatedField(many=True) ... By default, the field will uses the same accessor as it's field name to retrieve the relationship, so in this example, `Album` instances would need to have the `tracks` attribute for this relationship to work. @@ -316,7 +316,7 @@ The best way to ensure this is typically to make sure that the relationship on t Alternatively, you can use the `source` argument on the serializer field, to use a different accessor attribute than the field name. For example. class AlbumSerializer(serializers.ModelSerializer): - tracks = serializers.PrimaryKeyRelationship(many=True, source='track_set') + tracks = serializers.PrimaryKeyRelatedField(many=True, source='track_set') See the Django documentation on [reverse relationships][reverse-relationships] for more details. From 160d10d348bc44cb481d30592253ef7832210f4b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Feb 2013 08:46:00 +0000 Subject: [PATCH 07/68] Fix docstring --- rest_framework/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 1e481c874..fa7425828 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -211,13 +211,13 @@ class APIView(View): def get_parsers(self): """ - Instantiates and returns the list of renderers that this view can use. + Instantiates and returns the list of parsers that this view can use. """ return [parser() for parser in self.parser_classes] def get_authenticators(self): """ - Instantiates and returns the list of renderers that this view can use. + Instantiates and returns the list of authenticators that this view can use. """ return [auth() for auth in self.authentication_classes] From fc5f982ccc761efd5a6ee320dad7b97ebf9cfad8 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 20 Feb 2013 11:12:54 +0200 Subject: [PATCH 08/68] =?UTF-8?q?Don=E2=80=99t=20use=20my=20old=20nickname?= =?UTF-8?q?=20in=20credits.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/topics/credits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bb41ef5f5..990f3cb6e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -4,7 +4,7 @@ The following people have helped make REST framework great. * Tom Christie - [tomchristie] * Marko Tibold - [markotibold] -* Paul Bagwell - [pbgwl] +* Paul Miller - [paulmillr] * Sébastien Piquemal - [sebpiq] * Carmen Wick - [cwick] * Alex Ehlke - [aehlke] From 47a4f0863d08e4b839ea3bbd7308ecc0f995b7d9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Feb 2013 09:18:54 +0000 Subject: [PATCH 09/68] Update link to @paulmillr. Refs #668. --- docs/topics/credits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 990f3cb6e..e546548e6 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -142,7 +142,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [tomchristie]: https://github.com/tomchristie [markotibold]: https://github.com/markotibold -[pbgwl]: https://github.com/pbgwl +[paulmillr]: https://github.com/paulmillr [sebpiq]: https://github.com/sebpiq [cwick]: https://github.com/cwick [aehlke]: https://github.com/aehlke From 2fb6fa2dd3b336cc442e707dbb80a4d5616582a6 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Wed, 20 Feb 2013 17:15:12 +0600 Subject: [PATCH 10/68] Minimal forms appearance improvements --- rest_framework/static/rest_framework/css/default.css | 11 +++++++++++ rest_framework/static/rest_framework/js/default.js | 2 +- rest_framework/templates/rest_framework/base.html | 12 ++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index b2e41b994..731075271 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -150,6 +150,17 @@ html, body { margin: 0 auto -60px; } +.form-switcher { + margin-bottom: 0; +} + +.tab-content { + padding-top: 25px; + background: #fff; + border: 1px solid #ddd; + border-top: none; + border-radius: 0 0 4px 4px; +} #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 bc5b02928..484a3bdf1 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -4,4 +4,4 @@ $('.js-tooltip').tooltip({ delay: 1000 }); -$('#form-switcher a:first').tab('show'); \ No newline at end of file +$('.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 9d47a2edd..2fe7b6536 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,11 +125,11 @@ {% if post_form %}
-