From d972df7c9c1867b4a0a57307f423a488c4d4f4b1 Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Mon, 3 Nov 2014 14:43:53 +0100 Subject: [PATCH 01/30] Ability to override default method names by customizing it --- rest_framework/routers.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 169e6e8bc..d1c9fa1b9 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -176,23 +176,27 @@ class SimpleRouter(BaseRouter): if isinstance(route, DynamicDetailRoute): # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: + method_kwargs = getattr(viewset, methodname).kwargs + custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname initkwargs = route.initkwargs.copy() - initkwargs.update(getattr(viewset, methodname).kwargs) + initkwargs.update(method_kwargs) ret.append(Route( - url=replace_methodname(route.url, methodname), + url=replace_methodname(route.url, custom_method_name), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, methodname), + name=replace_methodname(route.name, custom_method_name), initkwargs=initkwargs, )) elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_route decorator) for httpmethods, methodname in list_routes: + method_kwargs = getattr(viewset, methodname).kwargs + custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname initkwargs = route.initkwargs.copy() - initkwargs.update(getattr(viewset, methodname).kwargs) + initkwargs.update(method_kwargs) ret.append(Route( - url=replace_methodname(route.url, methodname), + url=replace_methodname(route.url, custom_method_name), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, methodname), + name=replace_methodname(route.name, custom_method_name), initkwargs=initkwargs, )) else: From ea8c40520165fc33343fceb15221b770701bdedf Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Mon, 3 Nov 2014 14:44:47 +0100 Subject: [PATCH 02/30] Tests for validating custom_method_name router attribute --- tests/test_routers.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index f6f5a977a..d426f8320 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -8,6 +8,7 @@ from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.test import APIRequestFactory +from collections import namedtuple factory = APIRequestFactory() @@ -260,6 +261,14 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet): def detail_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) + @list_route(custom_method_name="list_custom-route") + def list_custom_route_get(self, request, *args, **kwargs): + return Response({'method': 'link1'}) + + @detail_route(custom_method_name="detail_custom-route") + def detail_custom_route_get(self, request, *args, **kwargs): + return Response({'method': 'link2'}) + class TestDynamicListAndDetailRouter(TestCase): def setUp(self): @@ -268,22 +277,33 @@ class TestDynamicListAndDetailRouter(TestCase): def test_list_and_detail_route_decorators(self): routes = self.router.get_routes(DynamicListAndDetailViewSet) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] + + MethodNamesMap = namedtuple('MethodNamesMap', 'method_name custom_method_name') # Make sure all these endpoints exist and none have been clobbered - for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']): + for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'), + MethodNamesMap('list_route_get', 'list_route_get'), + MethodNamesMap('list_route_post', 'list_route_post'), + MethodNamesMap('detail_custom_route_get', 'detail_custom-route'), + MethodNamesMap('detail_route_get', 'detail_route_get'), + MethodNamesMap('detail_route_post', 'detail_route_post') + ]): route = decorator_routes[i] # check url listing - if endpoint.startswith('list_'): + method_name = endpoint.method_name + custom_method_name = endpoint.custom_method_name + + if method_name.startswith('list_'): self.assertEqual(route.url, - '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint)) + '^{{prefix}}/{0}{{trailing_slash}}$'.format(custom_method_name)) else: self.assertEqual(route.url, - '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)) + '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(custom_method_name)) # check method to function mapping - if endpoint.endswith('_post'): + if method_name.endswith('_post'): method_map = 'post' else: method_map = 'get' - self.assertEqual(route.mapping[method_map], endpoint) + self.assertEqual(route.mapping[method_map], method_name) class TestRootWithAListlessViewset(TestCase): From 92ebeaa040f75dbc6142355fa25d89b4c990685b Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Fri, 19 Dec 2014 19:52:59 +0530 Subject: [PATCH 03/30] Change decorator attribute name to url_path per suggestions --- rest_framework/routers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index d1c9fa1b9..a213f62c7 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -177,26 +177,26 @@ class SimpleRouter(BaseRouter): # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: method_kwargs = getattr(viewset, methodname).kwargs - custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname + url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) ret.append(Route( - url=replace_methodname(route.url, custom_method_name), + url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, custom_method_name), + name=replace_methodname(route.name, url_path), initkwargs=initkwargs, )) elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_route decorator) for httpmethods, methodname in list_routes: method_kwargs = getattr(viewset, methodname).kwargs - custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname + url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) ret.append(Route( - url=replace_methodname(route.url, custom_method_name), + url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, custom_method_name), + name=replace_methodname(route.name, url_path), initkwargs=initkwargs, )) else: From 2448cc8e856369ca6fb99b848e10f8ff0105e925 Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Fri, 19 Dec 2014 19:53:48 +0530 Subject: [PATCH 04/30] Updated tests to use url_path attribute in list and detail decorators --- tests/test_routers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index d426f8320..73d10822a 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -261,11 +261,11 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet): def detail_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) - @list_route(custom_method_name="list_custom-route") + @list_route(url_path="list_custom-route") def list_custom_route_get(self, request, *args, **kwargs): return Response({'method': 'link1'}) - @detail_route(custom_method_name="detail_custom-route") + @detail_route(url_path="detail_custom-route") def detail_custom_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) @@ -278,7 +278,7 @@ class TestDynamicListAndDetailRouter(TestCase): routes = self.router.get_routes(DynamicListAndDetailViewSet) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] - MethodNamesMap = namedtuple('MethodNamesMap', 'method_name custom_method_name') + MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path') # Make sure all these endpoints exist and none have been clobbered for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'), MethodNamesMap('list_route_get', 'list_route_get'), @@ -290,14 +290,14 @@ class TestDynamicListAndDetailRouter(TestCase): route = decorator_routes[i] # check url listing method_name = endpoint.method_name - custom_method_name = endpoint.custom_method_name + url_path = endpoint.url_path if method_name.startswith('list_'): self.assertEqual(route.url, - '^{{prefix}}/{0}{{trailing_slash}}$'.format(custom_method_name)) + '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path)) else: self.assertEqual(route.url, - '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(custom_method_name)) + '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path)) # check method to function mapping if method_name.endswith('_post'): method_map = 'post' From a8a3fedb5c52cc62c6ecf59c4138e9a6ecf04806 Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Fri, 19 Dec 2014 20:16:46 +0530 Subject: [PATCH 05/30] Add url_path documention for detail_route decorator --- docs/tutorial/6-viewsets-and-routers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index cf37a2601..8e4e22f05 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -53,6 +53,8 @@ Notice that we've also used the `@detail_route` decorator to create a custom act Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests. +The URLs for custom actions by default depends on the method name itself. If you want to change the way url should be constructed, you can use `url_path` parameter of `@detail_route` and provide the string value for the same. + ## Binding ViewSets to URLs explicitly The handler methods only get bound to the actions when we define the URLConf. From 6aa0e307c99d0c17d7c48f2416472c7dbdcbbf8f Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 19 Dec 2014 20:31:21 +0530 Subject: [PATCH 06/30] Added documentation about url_path parameter for custom actions. --- docs/api-guide/routers.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 61a476b8b..63b8b59a8 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -68,6 +68,24 @@ The following URL pattern would additionally be generated: * URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` +If you did not like the default URL generated for your custom action, you could use `url_path` parameter with `@detail_route` or `@list_route` to customize it. + +For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write: + + from myapp.permissions import IsAdminOrIsSelf + from rest_framework.decorators import detail_route + + class UserViewSet(ModelViewSet): + ... + + @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password') + def set_password(self, request, pk=None): + ... + +Above example would instead generate following URL pattern: + +* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'` + For more information see the viewset documentation on [marking extra actions for routing][route-decorators]. # API Guide From b4a3e7f64096ea7106ff0d622bdf1c6e2e4e2895 Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 19 Dec 2014 21:20:19 +0530 Subject: [PATCH 07/30] Updates url_path info per suggestion --- docs/api-guide/routers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 63b8b59a8..87b6f15ac 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -68,7 +68,7 @@ The following URL pattern would additionally be generated: * URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` -If you did not like the default URL generated for your custom action, you could use `url_path` parameter with `@detail_route` or `@list_route` to customize it. +If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it. For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write: @@ -82,7 +82,7 @@ For example, if you want to change the URL for our custom action to `^users/{pk} def set_password(self, request, pk=None): ... -Above example would instead generate following URL pattern: +The above example would now generate the following URL pattern: * URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'` From 8f0fef4b75f5c999c13b5d37a263da3a3388142e Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 19 Dec 2014 21:22:10 +0530 Subject: [PATCH 08/30] Updated documentation on url_path per suggestions. --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 8e4e22f05..d2ee11028 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -53,7 +53,7 @@ Notice that we've also used the `@detail_route` decorator to create a custom act Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests. -The URLs for custom actions by default depends on the method name itself. If you want to change the way url should be constructed, you can use `url_path` parameter of `@detail_route` and provide the string value for the same. +The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument. ## Binding ViewSets to URLs explicitly From 77e3021fea3e30382b9770eac25371495e0b156b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 20 Dec 2014 16:26:51 +0000 Subject: [PATCH 09/30] Better behaviour with null and '' for blank HTML fields. --- rest_framework/fields.py | 13 +++++-------- tests/test_fields.py | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c40dc3fb3..aab80982a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -273,7 +273,11 @@ class Field(object): return empty return self.default_empty_html ret = dictionary[self.field_name] - return self.default_empty_html if (ret == '') else ret + if ret == '' and self.allow_null: + # If the field is blank, and null is a valid value then + # determine if we should use null instead. + return '' if getattr(self, 'allow_blank', False) else None + return ret return dictionary.get(self.field_name, empty) def get_attribute(self, instance): @@ -545,8 +549,6 @@ class CharField(Field): 'min_length': _('Ensure this field has at least {min_length} characters.') } initial = '' - coerce_blank_to_null = False - default_empty_html = '' def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) @@ -560,11 +562,6 @@ class CharField(Field): message = self.error_messages['min_length'].format(min_length=min_length) self.validators.append(MinLengthValidator(min_length, message=message)) - if self.allow_null and (not self.allow_blank) and (self.default is empty): - # HTML input cannot represent `None` values, so we need to - # forcibly coerce empty HTML values to `None` if `allow_null=True`. - self.default_empty_html = None - def run_validation(self, data=empty): # Test for the empty string here so that it does not get validated, # and so that subclasses do not need to handle it explicitly diff --git a/tests/test_fields.py b/tests/test_fields.py index 04c721d36..775d46184 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -223,8 +223,8 @@ class MockHTMLDict(dict): getlist = None -class TestCharHTMLInput: - def test_empty_html_checkbox(self): +class TestHTMLInput: + def test_empty_html_charfield(self): class TestSerializer(serializers.Serializer): message = serializers.CharField(default='happy') @@ -232,23 +232,31 @@ class TestCharHTMLInput: assert serializer.is_valid() assert serializer.validated_data == {'message': 'happy'} - def test_empty_html_checkbox_allow_null(self): + def test_empty_html_charfield_allow_null(self): class TestSerializer(serializers.Serializer): message = serializers.CharField(allow_null=True) - serializer = TestSerializer(data=MockHTMLDict()) + serializer = TestSerializer(data=MockHTMLDict({'message': ''})) assert serializer.is_valid() assert serializer.validated_data == {'message': None} - def test_empty_html_checkbox_allow_null_allow_blank(self): + def test_empty_html_datefield_allow_null(self): + class TestSerializer(serializers.Serializer): + expiry = serializers.DateField(allow_null=True) + + serializer = TestSerializer(data=MockHTMLDict({'expiry': ''})) + assert serializer.is_valid() + assert serializer.validated_data == {'expiry': None} + + def test_empty_html_charfield_allow_null_allow_blank(self): class TestSerializer(serializers.Serializer): message = serializers.CharField(allow_null=True, allow_blank=True) - serializer = TestSerializer(data=MockHTMLDict({})) + serializer = TestSerializer(data=MockHTMLDict({'message': ''})) assert serializer.is_valid() assert serializer.validated_data == {'message': ''} - def test_empty_html_required_false(self): + def test_empty_html_charfield_required_false(self): class TestSerializer(serializers.Serializer): message = serializers.CharField(required=False) From 03c4eb11305dcc9f366cdd008a5985bcf47c13ce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 20 Dec 2014 16:32:07 +0000 Subject: [PATCH 10/30] Use custom ListSerializer for pagination if one is specified on the serializer. --- rest_framework/pagination.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index fb4512854..f46b0dfa1 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -68,7 +68,12 @@ class BasePaginationSerializer(serializers.Serializer): except AttributeError: object_serializer = DefaultObjectSerializer - self.fields[results_field] = serializers.ListSerializer( + try: + list_serializer_class = object_serializer.Meta.list_serializer_class + except AttributeError: + list_serializer_class = serializers.ListSerializer + + self.fields[results_field] = list_serializer_class( child=object_serializer(), source='object_list' ) From 35696748603665526be7947e918d41856644ec52 Mon Sep 17 00:00:00 2001 From: Brian Stearns Date: Sun, 21 Dec 2014 18:53:35 -0500 Subject: [PATCH 11/30] use of double quotes broke the code highlighting. --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index f06db56cf..946e355da 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -480,7 +480,7 @@ Let's look at an example of serializing a class that represents an RGB color val class ColorField(serializers.Field): """ - Color objects are serialized into "rgb(#, #, #)" notation. + Color objects are serialized into 'rgb(#, #, #)' notation. """ def to_representation(self, obj): return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue) From 6c5ff712783ae7e6edebb52508f1d43249f1aa00 Mon Sep 17 00:00:00 2001 From: Remi Paulmier Date: Mon, 22 Dec 2014 18:05:07 +0100 Subject: [PATCH 12/30] fix the way to use textarea rather than input with models.TextField --- rest_framework/utils/field_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index fca97b4b3..b16e9df08 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -80,7 +80,7 @@ def get_field_kwargs(field_name, model_field): kwargs['decimal_places'] = decimal_places if isinstance(model_field, models.TextField): - kwargs['style'] = {'type': 'textarea'} + kwargs['style'] = {'base_template': 'textarea.html'} if isinstance(model_field, models.AutoField) or not model_field.editable: # If this field is read-only, then return early. From 18687f075d9fb998b82c6fb8f6cb37eb1ed7e5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Lang?= Date: Tue, 23 Dec 2014 12:22:10 -0300 Subject: [PATCH 13/30] Documented an optional argument of HyperlinkedIdentityField lookup_url_kwarg argument of HyperlinkedIdentityField wasn't documented --- docs/api-guide/relations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index e56db229a..50e3b7b59 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -231,6 +231,7 @@ This field is always read-only. * `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `-detail`. **required**. * `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`. +* `lookup_url_kwarg` - The name of the keyword argument defined in the URL conf that corresponds to the lookup field. Defaults to using the same value as `lookup_field`. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. --- From 399cb165b0ba26df6052c114eb77961dc578e686 Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Tue, 23 Dec 2014 12:11:45 -0800 Subject: [PATCH 14/30] Remove commented code (warning during compression) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manage.py compress —force causes a warning here. --- .../templates/rest_framework/vertical/list_fieldset.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html index 1d86c7f2b..82d7b5f41 100644 --- a/rest_framework/templates/rest_framework/vertical/list_fieldset.html +++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html @@ -1,8 +1,4 @@
{% if field.label %}{{ field.label }}{% endif %} -

Lists are not currently supported in HTML input.

From 35768344db45b9fa6bd94c3fd48d5e232027434e Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Tue, 23 Dec 2014 12:12:22 -0800 Subject: [PATCH 15/30] =?UTF-8?q?Remove=20=E2=80=98/=E2=80=98=20from=20ins?= =?UTF-8?q?ide=20variable=20block=20{{=20}}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manage.py compress —force causes a warning here. --- .../templates/rest_framework/inline/checkbox_multiple.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html index 6caf64403..093496862 100644 --- a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html +++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html @@ -5,7 +5,7 @@ {% for key, text in field.choices.items %}
From b32ecdefbace063c5b9b465af608ac6404795dd4 Mon Sep 17 00:00:00 2001 From: Remi Paulmier Date: Wed, 24 Dec 2014 14:07:28 +0100 Subject: [PATCH 16/30] modified the tests accordingly --- tests/test_model_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index da79164af..ee556dbcb 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -119,7 +119,7 @@ class TestRegularFieldMappings(TestCase): positive_small_integer_field = IntegerField() slug_field = SlugField(max_length=100) small_integer_field = IntegerField() - text_field = CharField(style={'type': 'textarea'}) + text_field = CharField(style={'base_template': 'textarea.html'}) time_field = TimeField() url_field = URLField(max_length=100) custom_field = ModelField(model_field=) From c2e00a075cb4b44c644ad5d62f2be0fd19e62c5f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Dec 2014 15:25:13 +0000 Subject: [PATCH 17/30] Paginated serializers should get context. --- rest_framework/pagination.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index f46b0dfa1..f31e5fa4c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -77,6 +77,7 @@ class BasePaginationSerializer(serializers.Serializer): child=object_serializer(), source='object_list' ) + self.fields[results_field].bind(field_name=results_field, parent=self) class PaginationSerializer(BasePaginationSerializer): From 00531ec937206e7e0af949c67872c915d0752b5a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Dec 2014 15:48:16 +0000 Subject: [PATCH 18/30] Release notes on non-text detail arguments. Closes #2341. --- docs/topics/3.0-announcement.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 0710766f7..68d247827 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -940,6 +940,7 @@ The default JSON renderer will return float objects for un-coerced `Decimal` ins * The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1. * Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand. * Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them. +* `APIException` subclasses could previously take could previously take any arbitrary type in the `detail` argument. These exceptions now use translatable text strings, and as a result call `force_text` on the `detail` argument, which *must be a string*. If you need complex arguments to an `APIException` class, you should subclass it and override the `__init__()` method. Typically you'll instead want to use a custom exception handler to provide for non-standard error responses. --- From 5b5652594a9c000d8e925d35efa03be27c28c077 Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Fri, 26 Dec 2014 22:24:31 -0700 Subject: [PATCH 19/30] Typo manger => manager --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index b9f0e7bc0..f88ec51f2 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -384,7 +384,7 @@ This manager class now more nicely encapsulates that user instances and profile has_support_contract=validated_data['profile']['has_support_contract'] ) -For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manger classes](encapsulation-blogpost). +For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manager classes](encapsulation-blogpost). ## Dealing with multiple objects From a636320ff3b381a6d7d8685f1b4fba8bdd6c8b94 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 11:02:19 +0000 Subject: [PATCH 20/30] Add import notes in docs. Closes #2357 --- docs/api-guide/generic-views.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index f5bbdfdda..6374e3052 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -214,6 +214,8 @@ You won't typically need to override the following methods, although you might n The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior. +The mixin classes can be imported from `rest_framework.mixins`. + ## ListModelMixin Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset. @@ -258,6 +260,8 @@ If an object is deleted this returns a `204 No Content` response, otherwise it w The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior. +The view classes can be imported from `rest_framework.generics`. + ## CreateAPIView Used for **create-only** endpoints. From ef2eff2abac64ffbed621bb9a72a2229841a1db1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 11:07:38 +0000 Subject: [PATCH 21/30] Only pass max_length for CharField. Closes #2317. --- rest_framework/utils/field_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b16e9df08..b2f4dd80e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -106,7 +106,7 @@ def get_field_kwargs(field_name, model_field): # Ensure that max_length is passed explicitly as a keyword arg, # rather than as a validator. max_length = getattr(model_field, 'max_length', None) - if max_length is not None: + if max_length is not None and isinstance(model_field, models.CharField): kwargs['max_length'] = max_length validator_kwarg = [ validator for validator in validator_kwarg From 7b42c5ed17a2430d66da88932ad4e81492d9b914 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 11:14:32 +0000 Subject: [PATCH 22/30] Remove broken test. Closes #2359. --- tests/test_routers.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index 06ab8103a..2b6cd7d28 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -305,19 +305,3 @@ class TestDynamicListAndDetailRouter(TestCase): else: method_map = 'get' self.assertEqual(route.mapping[method_map], method_name) - - -class TestRootWithAListlessViewset(TestCase): - def setUp(self): - class NoteViewSet(mixins.RetrieveModelMixin, - viewsets.GenericViewSet): - model = RouterTestModel - - self.router = DefaultRouter() - self.router.register(r'notes', NoteViewSet) - self.view = self.router.urls[0].callback - - def test_api_root(self): - request = factory.get('/') - response = self.view(request) - self.assertEqual(response.data, {}) From 8dc95ee22181de6e38c7187426bca9fcee9d7927 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 11:24:49 +0000 Subject: [PATCH 23/30] Add notes on include and namespacing. Closes #2335. --- docs/api-guide/routers.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 6819adb6a..3a8a8f6cd 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -49,6 +49,38 @@ This means you'll need to explicitly set the `base_name` argument when registeri --- +### Using `include` with routers + +The `.urls` attribute on a router instance is simply a standard list of URL patterns. There are a number of different styles for how you can include these URLs. + +For example, you can append `router.urls` to a list of existing views… + + router = routers.SimpleRouter() + router.register(r'users', UserViewSet) + router.register(r'accounts', AccountViewSet) + + urlpatterns = [ + url(r'^forgot-password/$, ForgotPasswordFormView.as_view(), + ] + + urlpatterns += router.urls + +Alternatively you can use Django's `include` function, like so… + + urlpatterns = [ + url(r'^forgot-password/$, ForgotPasswordFormView.as_view(), + url(r'^', include(router.urls)) + ] + +Router URL patterns can also be namespaces. + + urlpatterns = [ + url(r'^forgot-password/$, ForgotPasswordFormView.as_view(), + url(r'^api/', include(router.urls, namespace='api')) + ] + +If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view. + ### Extra link and actions Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed. From 67fc002f91e5dc617dab45895ded32d6be6c2a40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 11:26:38 +0000 Subject: [PATCH 24/30] Drop unused import --- tests/test_routers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index 2b6cd7d28..fc22a8d95 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -3,7 +3,7 @@ from django.conf.urls import patterns, url, include from django.db import models from django.test import TestCase from django.core.exceptions import ImproperlyConfigured -from rest_framework import serializers, viewsets, mixins, permissions +from rest_framework import serializers, viewsets, permissions from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter From efa5942ce1c5d2286fd91994b52fb73a5690426c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 12:02:52 +0000 Subject: [PATCH 25/30] Support namespaced router URLs with DefaultRouter. --- rest_framework/compat.py | 10 +++++ rest_framework/routers.py | 5 ++- tests/test_routers.py | 94 ++++++++++++++++++++++++++------------- 3 files changed, 76 insertions(+), 33 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 69fdd7936..ba26a3cd7 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -50,6 +50,16 @@ except ImportError: from django.http import HttpResponse as HttpResponseBase +# request only provides `resolver_match` from 1.5 onwards. +def get_resolver_match(request): + try: + return request.resolver_match + except AttributeError: + # Django < 1.5 + from django.core.urlresolvers import resolve + return resolve(request.path_info) + + # django-filter is optional try: import django_filters diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 1cb65b1c0..61f3ccab0 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -21,7 +21,7 @@ from django.conf.urls import patterns, url from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import NoReverseMatch from rest_framework import views -from rest_framework.compat import OrderedDict +from rest_framework.compat import get_resolver_match, OrderedDict from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.urlpatterns import format_suffix_patterns @@ -292,7 +292,10 @@ class DefaultRouter(SimpleRouter): def get(self, request, *args, **kwargs): ret = OrderedDict() + namespace = get_resolver_match(request).namespace for key, url_name in api_root_dict.items(): + if namespace: + url_name = namespace + ':' + url_name try: ret[key] = reverse( url_name, diff --git a/tests/test_routers.py b/tests/test_routers.py index fc22a8d95..86113f5d7 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from django.conf.urls import patterns, url, include +from django.conf.urls import url, include from django.db import models from django.test import TestCase from django.core.exceptions import ImproperlyConfigured @@ -12,7 +12,42 @@ from collections import namedtuple factory = APIRequestFactory() -urlpatterns = patterns('',) + +class RouterTestModel(models.Model): + uuid = models.CharField(max_length=20) + text = models.CharField(max_length=200) + + +class NoteSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid') + + class Meta: + model = RouterTestModel + fields = ('url', 'uuid', 'text') + + +class NoteViewSet(viewsets.ModelViewSet): + queryset = RouterTestModel.objects.all() + serializer_class = NoteSerializer + lookup_field = 'uuid' + + +class MockViewSet(viewsets.ModelViewSet): + queryset = None + serializer_class = None + + +notes_router = SimpleRouter() +notes_router.register(r'notes', NoteViewSet) + +namespaced_router = DefaultRouter() +namespaced_router.register(r'example', MockViewSet, base_name='example') + +urlpatterns = [ + url(r'^non-namespaced/', include(namespaced_router.urls)), + url(r'^namespaced/', include(namespaced_router.urls, namespace='example')), + url(r'^example/', include(notes_router.urls)), +] class BasicViewSet(viewsets.ViewSet): @@ -64,9 +99,26 @@ class TestSimpleRouter(TestCase): self.assertEqual(route.mapping[method], endpoint) -class RouterTestModel(models.Model): - uuid = models.CharField(max_length=20) - text = models.CharField(max_length=200) +class TestRootView(TestCase): + urls = 'tests.test_routers' + + def test_retrieve_namespaced_root(self): + response = self.client.get('/namespaced/') + self.assertEqual( + response.data, + { + "example": "http://testserver/namespaced/example/", + } + ) + + def test_retrieve_non_namespaced_root(self): + response = self.client.get('/non-namespaced/') + self.assertEqual( + response.data, + { + "example": "http://testserver/non-namespaced/example/", + } + ) class TestCustomLookupFields(TestCase): @@ -76,51 +128,29 @@ class TestCustomLookupFields(TestCase): urls = 'tests.test_routers' def setUp(self): - class NoteSerializer(serializers.HyperlinkedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid') - - class Meta: - model = RouterTestModel - fields = ('url', 'uuid', 'text') - - class NoteViewSet(viewsets.ModelViewSet): - queryset = RouterTestModel.objects.all() - serializer_class = NoteSerializer - lookup_field = 'uuid' - - self.router = SimpleRouter() - self.router.register(r'notes', NoteViewSet) - - from tests import test_routers - urls = getattr(test_routers, 'urlpatterns') - urls += patterns( - '', - url(r'^', include(self.router.urls)), - ) - RouterTestModel.objects.create(uuid='123', text='foo bar') def test_custom_lookup_field_route(self): - detail_route = self.router.urls[-1] + detail_route = notes_router.urls[-1] detail_url_pattern = detail_route.regex.pattern self.assertIn('', detail_url_pattern) def test_retrieve_lookup_field_list_view(self): - response = self.client.get('/notes/') + response = self.client.get('/example/notes/') self.assertEqual( response.data, [{ - "url": "http://testserver/notes/123/", + "url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar" }] ) def test_retrieve_lookup_field_detail_view(self): - response = self.client.get('/notes/123/') + response = self.client.get('/example/notes/123/') self.assertEqual( response.data, { - "url": "http://testserver/notes/123/", + "url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar" } ) From d8e66970a11ec2d4b66f0cf56950f2cc83e83224 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 12:14:07 +0000 Subject: [PATCH 26/30] Note on using i18n_patterns with format_suffix_patterns. Closes #2278. --- docs/api-guide/format-suffixes.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md index 20c1e9952..35dbcd39c 100644 --- a/docs/api-guide/format-suffixes.md +++ b/docs/api-guide/format-suffixes.md @@ -55,6 +55,18 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se Also note that `format_suffix_patterns` does not support descending into `include` URL patterns. +### Using with `i18n_patterns` + +If using the `i18n_patterns` function provided by Django, as well as `format_suffix_patterns` you should make sure that the `i18n_patterns` function is applied as the final, or outermost function. For example: + + url patterns = [ + … + ] + + urlpatterns = i18n_patterns( + format_suffix_patterns(urlpatterns, allowed=['json', 'html']) + ) + --- ## Accept headers vs. format suffixes From 5d8c45681a945b955d9336b0fd1e4ebccf0df895 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 18:48:42 +0000 Subject: [PATCH 27/30] Update copryright for 2015. Closes #2247. --- README.md | 2 +- docs/index.md | 2 +- rest_framework/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index df0a4086a..8fc11c30f 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou # License -Copyright (c) 2011-2014, Tom Christie +Copyright (c) 2011-2015, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/index.md b/docs/index.md index 8a96fc9fb..55129df18 100644 --- a/docs/index.md +++ b/docs/index.md @@ -235,7 +235,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou ## License -Copyright (c) 2011-2014, Tom Christie +Copyright (c) 2011-2015, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 6808b74b0..dec89b3e9 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -11,7 +11,7 @@ __title__ = 'Django REST framework' __version__ = '3.0.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' -__copyright__ = 'Copyright 2011-2014 Tom Christie' +__copyright__ = 'Copyright 2011-2015 Tom Christie' # Version synonym VERSION = __version__ From a7479721c844926f377085d8c336a2f60b7b2a38 Mon Sep 17 00:00:00 2001 From: Kyle Valade Date: Mon, 29 Dec 2014 00:35:00 -0800 Subject: [PATCH 28/30] First pass at refactoring get_field_info in utils.model_meta --- rest_framework/utils/model_meta.py | 57 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index c98725c66..375d2e8c6 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -35,7 +35,7 @@ def _resolve_model(obj): Resolve supplied `obj` to a Django model class. `obj` must be a Django model class itself, or a string - representation of one. Useful in situtations like GH #1225 where + representation of one. Useful in situations like GH #1225 where Django may not have resolved a string-based reference to a model in another model's foreign key definition. @@ -56,23 +56,44 @@ def _resolve_model(obj): def get_field_info(model): """ - Given a model class, returns a `FieldInfo` instance containing metadata - about the various field types on the model. + Given a model class, returns a `FieldInfo` instance, which is a + `namedtuple`, containing metadata about the various field types on the model + including information about their relationships. """ opts = model._meta.concrete_model._meta - # Deal with the primary key. + pk = _get_pk(opts) + fields = _get_fields(opts) + forward_relations = _get_forward_relationships(opts) + reverse_relations = _get_reverse_relationships(opts) + fields_and_pk = _merge_fields_and_pk(pk, fields) + relationships = _merge_relationships(forward_relations, reverse_relations) + + return FieldInfo(pk, fields, forward_relations, reverse_relations, + fields_and_pk, relationships) + + +def _get_pk(opts): pk = opts.pk while pk.rel and pk.rel.parent_link: - # If model is a child via multitable inheritance, use parent's pk. + # If model is a child via multi-table inheritance, use parent's pk. pk = pk.rel.to._meta.pk - # Deal with regular fields. + return pk + + +def _get_fields(opts): fields = OrderedDict() for field in [field for field in opts.fields if field.serialize and not field.rel]: fields[field.name] = field - # Deal with forward relationships. + return fields + + +def _get_forward_relationships(opts): + """ + Returns an `OrderedDict` of field names to `RelationInfo`. + """ forward_relations = OrderedDict() for field in [field for field in opts.fields if field.serialize and field.rel]: forward_relations[field.name] = RelationInfo( @@ -93,7 +114,13 @@ def get_field_info(model): ) ) - # Deal with reverse relationships. + return forward_relations + + +def _get_reverse_relationships(opts): + """ + Returns an `OrderedDict` of field names to `RelationInfo`. + """ reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() @@ -117,18 +144,20 @@ def get_field_info(model): ) ) - # Shortcut that merges both regular fields and the pk, - # for simplifying regular field lookup. + return reverse_relations + + +def _merge_fields_and_pk(pk, fields): fields_and_pk = OrderedDict() fields_and_pk['pk'] = pk fields_and_pk[pk.name] = pk fields_and_pk.update(fields) - # Shortcut that merges both forward and reverse relationships + return fields_and_pk - relations = OrderedDict( + +def _merge_relationships(forward_relations, reverse_relations): + return OrderedDict( list(forward_relations.items()) + list(reverse_relations.items()) ) - - return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations) From 336faf5a861fad2e4364a68fbf0747bef4457358 Mon Sep 17 00:00:00 2001 From: Rob Terhaar Date: Thu, 1 Jan 2015 21:01:16 -0500 Subject: [PATCH 29/30] fix widget style formatting --- docs/tutorial/1-serialization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index ff507a2b8..60a3d9897 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -124,7 +124,7 @@ The first part of the serializer class defines the fields that get serialized/de A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`. -The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `style={'type': 'textarea'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial. +The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `{'base_template': 'textarea.html'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial. We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit. @@ -206,7 +206,7 @@ One nice property that serializers have is that you can inspect all the fields i SnippetSerializer(): id = IntegerField(label='ID', read_only=True) title = CharField(allow_blank=True, max_length=100, required=False) - code = CharField(style={'type': 'textarea'}) + code = CharField(style={'base_template': 'textarea.html'}) linenos = BooleanField(required=False) language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')... style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')... From 8cf37449715c32c4a692667814466c7f32e8734f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 5 Jan 2015 10:52:18 +0000 Subject: [PATCH 30/30] Ensure no invalid min_length/min_value/max_value arguments. Closes #2369. --- rest_framework/utils/field_mapping.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b2f4dd80e..cba40d318 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -10,6 +10,11 @@ from rest_framework.validators import UniqueValidator import inspect +NUMERIC_FIELD_TYPES = ( + models.IntegerField, models.FloatField, models.DecimalField +) + + class ClassLookupDict(object): """ Takes a dictionary with classes as keys. @@ -119,7 +124,7 @@ def get_field_kwargs(field_name, model_field): validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MinLengthValidator) ), None) - if min_length is not None: + if min_length is not None and isinstance(model_field, models.CharField): kwargs['min_length'] = min_length validator_kwarg = [ validator for validator in validator_kwarg @@ -132,7 +137,7 @@ def get_field_kwargs(field_name, model_field): validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MaxValueValidator) ), None) - if max_value is not None: + if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['max_value'] = max_value validator_kwarg = [ validator for validator in validator_kwarg @@ -145,7 +150,7 @@ def get_field_kwargs(field_name, model_field): validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MinValueValidator) ), None) - if min_value is not None: + if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['min_value'] = min_value validator_kwarg = [ validator for validator in validator_kwarg