From 3a2ad8e68c669eebcc9847e6c2a664e323e8da1d Mon Sep 17 00:00:00 2001 From: imdark Date: Wed, 17 May 2017 11:49:30 -0700 Subject: [PATCH 01/28] in order to solve the memory leak at #5146 Large encoded string take a very long time to to release from memory, but if we just pass the stream directly into json.load we get much better memory performance. --- rest_framework/parsers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 238382364..1a4c24387 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -22,6 +22,7 @@ from django.utils.six.moves.urllib import parse as urlparse from rest_framework import renderers from rest_framework.exceptions import ParseError +import codecs class DataAndFiles(object): @@ -61,8 +62,8 @@ class JSONParser(BaseParser): encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) try: - data = stream.read().decode(encoding) - return json.loads(data) + decoded_stream = codecs.decode(stream, encoding) + return json.load(decoded_stream) except ValueError as exc: raise ParseError('JSON parse error - %s' % six.text_type(exc)) From 53b3b83b0460e99d22c96cc75625358cdedb96a7 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 22 May 2017 11:55:19 +0200 Subject: [PATCH 02/28] Add `generator_class` parameter to docs. `get_schema_view` accepts `generator_class` parameter --- docs/api-guide/schemas.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index afa058d94..aa830e47d 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -185,6 +185,12 @@ to be exposed in the schema: patterns=schema_url_patterns, ) +#### `generator_class` + +May be used to specify a `SchemaGenerator` subclass to be passed to the +`SchemaView`. + + ## Using an explicit schema view From d1093b5326b068ec33139e2c3349a56550078122 Mon Sep 17 00:00:00 2001 From: Azim Khakulov Date: Tue, 23 May 2017 02:08:20 +0200 Subject: [PATCH 03/28] Added documentation from where to import get_schema_view --- docs/api-guide/schemas.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index f43ff56bd..0a6f9eb2d 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -107,6 +107,8 @@ add a schema to your API, depending on exactly what you need. The simplest way to include a schema in your project is to use the `get_schema_view()` function. + from rest_framework.schemas import get_schema_view + schema_view = get_schema_view(title="Server Monitoring API") urlpatterns = [ @@ -161,6 +163,7 @@ ROOT_URLCONF setting. May be used to pass the set of renderer classes that can be used to render the API root endpoint. + from rest_framework.schemas import get_schema_view from rest_framework.renderers import CoreJSONRenderer from my_custom_package import APIBlueprintRenderer From 99782c2160de49c106fe9e9ce087de3fc36e4cf5 Mon Sep 17 00:00:00 2001 From: Tadhg O'Higgins Date: Wed, 24 May 2017 16:46:18 -0700 Subject: [PATCH 04/28] Add tests for HTML_CUTOFF setting and fix issue where setting it to None would raise an exception. --- rest_framework/relations.py | 23 +++++++++------ tests/test_relations.py | 58 ++++++++++++++++++++++++++++++++++++- tests/utils.py | 3 ++ 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 54e67cd16..4d3bdba1d 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -75,7 +75,8 @@ class PKOnlyObject(object): # rather than the parent serializer. MANY_RELATION_KWARGS = ( 'read_only', 'write_only', 'required', 'default', 'initial', 'source', - 'label', 'help_text', 'style', 'error_messages', 'allow_empty' + 'label', 'help_text', 'style', 'error_messages', 'allow_empty', + 'html_cutoff', 'html_cutoff_text' ) @@ -86,10 +87,12 @@ class RelatedField(Field): def __init__(self, **kwargs): self.queryset = kwargs.pop('queryset', self.queryset) - self.html_cutoff = kwargs.pop( - 'html_cutoff', - self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF) - ) + + cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF + if cutoff_from_settings is not None: + cutoff_from_settings = int(cutoff_from_settings) + self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings) + self.html_cutoff_text = kwargs.pop( 'html_cutoff_text', self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) @@ -466,10 +469,12 @@ class ManyRelatedField(Field): def __init__(self, child_relation=None, *args, **kwargs): self.child_relation = child_relation self.allow_empty = kwargs.pop('allow_empty', True) - self.html_cutoff = kwargs.pop( - 'html_cutoff', - self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF) - ) + + cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF + if cutoff_from_settings is not None: + cutoff_from_settings = int(cutoff_from_settings) + self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings) + self.html_cutoff_text = kwargs.pop( 'html_cutoff_text', self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) diff --git a/tests/test_relations.py b/tests/test_relations.py index c903ee557..32e9b9cc7 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,12 +1,13 @@ import uuid import pytest +from _pytest.monkeypatch import MonkeyPatch from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.test import override_settings from django.utils.datastructures import MultiValueDict -from rest_framework import serializers +from rest_framework import relations, serializers from rest_framework.fields import empty from rest_framework.test import APISimpleTestCase @@ -25,6 +26,61 @@ class TestStringRelatedField(APISimpleTestCase): assert representation == '' +class MockApiSettings(object): + def __init__(self, cutoff, cutoff_text): + self.HTML_SELECT_CUTOFF = cutoff + self.HTML_SELECT_CUTOFF_TEXT = cutoff_text + + +class TestRelatedFieldHTMLCutoff(APISimpleTestCase): + def setUp(self): + self.queryset = MockQueryset([ + MockObject(pk=i, name=str(i)) for i in range(0, 1100) + ]) + self.monkeypatch = MonkeyPatch() + + def test_no_settings(self): + # The default is 1,000, so sans settings it should be 1,000 plus one. + for many in (False, True): + field = serializers.PrimaryKeyRelatedField(queryset=self.queryset, + many=many) + options = list(field.iter_options()) + assert len(options) == 1001 + assert options[-1].display_text == "More than 1000 items..." + + def test_settings_cutoff(self): + self.monkeypatch.setattr(relations, "api_settings", + MockApiSettings(2, "Cut Off")) + for many in (False, True): + field = serializers.PrimaryKeyRelatedField(queryset=self.queryset, + many=many) + options = list(field.iter_options()) + assert len(options) == 3 # 2 real items plus the 'Cut Off' item. + assert options[-1].display_text == "Cut Off" + + def test_settings_cutoff_none(self): + # Setting it to None should mean no limit; the default limit is 1,000. + self.monkeypatch.setattr(relations, "api_settings", + MockApiSettings(None, "Cut Off")) + for many in (False, True): + field = serializers.PrimaryKeyRelatedField(queryset=self.queryset, + many=many) + options = list(field.iter_options()) + assert len(options) == 1100 + + def test_settings_kwargs_cutoff(self): + # The explicit argument should override the settings. + self.monkeypatch.setattr(relations, "api_settings", + MockApiSettings(2, "Cut Off")) + for many in (False, True): + field = serializers.PrimaryKeyRelatedField(queryset=self.queryset, + many=many, + html_cutoff=100) + options = list(field.iter_options()) + assert len(options) == 101 + assert options[-1].display_text == "Cut Off" + + class TestPrimaryKeyRelatedField(APISimpleTestCase): def setUp(self): self.queryset = MockQueryset([ diff --git a/tests/utils.py b/tests/utils.py index 52582f093..5fb0723f8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -20,6 +20,9 @@ class MockQueryset(object): def __init__(self, iterable): self.items = iterable + def __getitem__(self, val): + return self.items[val] + def get(self, **lookup): for item in self.items: if all([ From 9a2281167178999ad0472d3c9862af91c077322c Mon Sep 17 00:00:00 2001 From: imdark Date: Wed, 24 May 2017 17:56:49 -0700 Subject: [PATCH 05/28] modified to use a reader modified to use a reader since direct decoding is not supported --- rest_framework/parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 1a4c24387..817efd2da 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -7,6 +7,7 @@ on the request, such as form content or json encoded data. from __future__ import unicode_literals import json +import codecs from django.conf import settings from django.core.files.uploadhandler import StopFutureHandlers @@ -22,7 +23,6 @@ from django.utils.six.moves.urllib import parse as urlparse from rest_framework import renderers from rest_framework.exceptions import ParseError -import codecs class DataAndFiles(object): @@ -62,7 +62,7 @@ class JSONParser(BaseParser): encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) try: - decoded_stream = codecs.decode(stream, encoding) + decoded_stream = codecs.getreader(encoding)(stream) return json.load(decoded_stream) except ValueError as exc: raise ParseError('JSON parse error - %s' % six.text_type(exc)) From cdeab1c490fa1073ce985b227e85bbdc56cb08c2 Mon Sep 17 00:00:00 2001 From: imdark Date: Wed, 24 May 2017 18:12:38 -0700 Subject: [PATCH 06/28] fixed to pass isort linting --- rest_framework/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 817efd2da..0e40e1a7a 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -6,8 +6,8 @@ on the request, such as form content or json encoded data. """ from __future__ import unicode_literals -import json import codecs +import json from django.conf import settings from django.core.files.uploadhandler import StopFutureHandlers From 94c37c09c58cc37d61c5a678b37a0111c90936d6 Mon Sep 17 00:00:00 2001 From: Levi Cameron Date: Thu, 25 May 2017 20:07:34 +1000 Subject: [PATCH 07/28] Fix browsable API not supporting multipart/form-data correctly - Autodetect missing boundary parameter for Content-Type header - textarea value normalises EOL chars to \n when multipart/form-data requires \r\n --- rest_framework/static/rest_framework/js/ajax-form.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/static/rest_framework/js/ajax-form.js b/rest_framework/static/rest_framework/js/ajax-form.js index ce17729d1..88cb133bf 100644 --- a/rest_framework/static/rest_framework/js/ajax-form.js +++ b/rest_framework/static/rest_framework/js/ajax-form.js @@ -37,6 +37,17 @@ function doAjaxSubmit(e) { if (contentType) { data = form.find('[data-override="content"]').val() || '' + + if (contentType === 'multipart/form-data') { + // We need to add a boundary parameter to the header + var re = /^--([0-9A-Z'()+_,-./:=?]{1,70})[ \f\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*$/im; + var boundary = re.exec(data); + if (boundary !== null) { + contentType += '; boundary="' + boundary[1] + '"'; + } + // Fix textarea.value EOL normalisation (multipart/form-data should use CR+NL, not NL) + data = data.replace(/\n/g, '\r\n'); + } } else { contentType = form.attr('enctype') || form.attr('encoding') From e6c9f89a123756382cbfa7196ae631e41ef3feaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=99=98?= Date: Thu, 25 May 2017 19:13:45 +0900 Subject: [PATCH 08/28] Fixed curly bracket in regexp of @list_route --- rest_framework/routers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index fce968aa0..81f8ebf78 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -35,6 +35,12 @@ DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwarg DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs']) +def replace_curly_brackets(url_path): + if ('{' and '}') in url_path: + url_path = url_path.replace('{', '{{').replace('}', '}}') + return url_path + + def replace_methodname(format_string, methodname): """ Partially format a format_string, swapping out any @@ -178,6 +184,7 @@ class SimpleRouter(BaseRouter): initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) url_path = initkwargs.pop("url_path", None) or methodname + url_path = replace_curly_brackets(url_path) url_name = initkwargs.pop("url_name", None) or url_path ret.append(Route( url=replace_methodname(route.url, url_path), From a002bb5c67a8d033463d30ea6527fb2825b61adf Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 26 May 2017 09:25:43 +0200 Subject: [PATCH 09/28] Fixed test_hyperlinked_related_lookup_url_encoded_exists. Space character ' ' is prohibited in IRIs, therefore we shouldn't rely on encoding '%20' to ' ' in the HyperlinkedRelatedField tests. --- tests/test_relations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_relations.py b/tests/test_relations.py index c903ee557..1868374fc 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -96,7 +96,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): def setUp(self): self.queryset = MockQueryset([ MockObject(pk=1, name='foobar'), - MockObject(pk=2, name='baz qux'), + MockObject(pk=2, name='bazABCqux'), ]) self.field = serializers.HyperlinkedRelatedField( view_name='example', @@ -116,7 +116,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): assert instance is self.queryset.items[0] def test_hyperlinked_related_lookup_url_encoded_exists(self): - instance = self.field.to_internal_value('http://example.org/example/baz%20qux/') + instance = self.field.to_internal_value('http://example.org/example/baz%41%42%43qux/') assert instance is self.queryset.items[1] def test_hyperlinked_related_lookup_does_not_exist(self): From 04adfb9c94b2fa68c4f9461fce36091b803840b1 Mon Sep 17 00:00:00 2001 From: Dryice Liu Date: Sun, 28 May 2017 04:14:56 +0800 Subject: [PATCH 10/28] make sure max_length is in FileField kwargs --- rest_framework/utils/field_mapping.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 44c6d4b70..dff33d8b3 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -192,7 +192,8 @@ def get_field_kwargs(field_name, model_field): # rather than as a validator. max_length = getattr(model_field, 'max_length', None) if max_length is not None and (isinstance(model_field, models.CharField) or - isinstance(model_field, models.TextField)): + isinstance(model_field, models.TextField) or + isinstance(model_field, models.FileField)): kwargs['max_length'] = max_length validator_kwarg = [ validator for validator in validator_kwarg From 973860d9fe91f59d9c46e086fb5ef1aa839a4bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=99=98?= Date: Sun, 28 May 2017 18:38:09 +0900 Subject: [PATCH 11/28] Added: test for list_route and detail_route with regex url_path --- tests/test_routers.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_routers.py b/tests/test_routers.py index 97f43b91a..fee39b2b3 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -65,6 +65,19 @@ class EmptyPrefixViewSet(viewsets.ModelViewSet): return self.queryset[index] +class RegexUrlPathViewSet(viewsets.ViewSet): + @list_route(url_path='list/(?P[0-9]{4})') + def regex_url_path_list(self, request, *args, **kwargs): + kwarg = self.kwargs.get('kwarg', '') + return Response({'kwarg': kwarg}) + + @detail_route(url_path='detail/(?P[0-9]{4})') + def regex_url_path_detail(self, request, *args, **kwargs): + pk = self.kwargs.get('pk', '') + kwarg = self.kwargs.get('kwarg', '') + return Response({'pk': pk, 'kwarg': kwarg}) + + notes_router = SimpleRouter() notes_router.register(r'notes', NoteViewSet) @@ -80,6 +93,9 @@ empty_prefix_urls = [ url(r'^', include(empty_prefix_router.urls)), ] +regex_url_path_router = SimpleRouter() +regex_url_path_router.register(r'', RegexUrlPathViewSet, base_name='regex') + urlpatterns = [ url(r'^non-namespaced/', include(namespaced_router.urls)), url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')), @@ -87,6 +103,7 @@ urlpatterns = [ url(r'^example2/', include(kwarged_notes_router.urls)), url(r'^empty-prefix/', include(empty_prefix_urls)), + url(r'^regex/', include(regex_url_path_router.urls)) ] @@ -402,3 +419,19 @@ class TestEmptyPrefix(TestCase): response = self.client.get('/empty-prefix/1/') assert response.status_code == 200 assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'} + + +@override_settings(ROOT_URLCONF='tests.test_routers') +class TestRegexUrlPath(TestCase): + def test_regex_url_path_list(self): + kwarg = '1234' + response = self.client.get('/regex/list/{}/'.format(kwarg)) + assert response.status_code == 200 + assert json.loads(response.content.decode('utf-8')) == {'kwarg': kwarg} + + def test_regex_url_path_detail(self): + pk = '1' + kwarg = '1234' + response = self.client.get('/regex/{}/detail/{}/'.format(pk, kwarg)) + assert response.status_code == 200 + assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg} From 836328594b2ac9a81cf6e1ecf6cdc204e7387326 Mon Sep 17 00:00:00 2001 From: Dryice Liu Date: Mon, 29 May 2017 08:27:07 +0800 Subject: [PATCH 12/28] add test --- tests/test_model_serializer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 4aa0fa35e..ba3edd389 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -63,6 +63,7 @@ class RegularFieldsModel(models.Model): slug_field = models.SlugField(max_length=100) small_integer_field = models.SmallIntegerField() text_field = models.TextField(max_length=100) + file_field = models.FileField(max_length=100) time_field = models.TimeField() url_field = models.URLField(max_length=100) custom_field = CustomField() @@ -181,6 +182,7 @@ class TestRegularFieldMappings(TestCase): slug_field = SlugField(max_length=100) small_integer_field = IntegerField() text_field = CharField(max_length=100, style={'base_template': 'textarea.html'}) + file_field = FileField(max_length=100) time_field = TimeField() url_field = URLField(max_length=100) custom_field = ModelField(model_field=) From 0ad017a5735a532280c8c56ad8c66f950f0b22f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=99=98?= Date: Mon, 29 May 2017 20:55:06 +0900 Subject: [PATCH 13/28] requested changes --- rest_framework/routers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 81f8ebf78..87e58b015 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -35,7 +35,10 @@ DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwarg DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs']) -def replace_curly_brackets(url_path): +def escape_curly_brackets(url_path): + """ + Double brackets in regex of url_path for escape string formatting + """ if ('{' and '}') in url_path: url_path = url_path.replace('{', '{{').replace('}', '}}') return url_path @@ -184,7 +187,7 @@ class SimpleRouter(BaseRouter): initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) url_path = initkwargs.pop("url_path", None) or methodname - url_path = replace_curly_brackets(url_path) + url_path = escape_curly_brackets(url_path) url_name = initkwargs.pop("url_name", None) or url_path ret.append(Route( url=replace_methodname(route.url, url_path), From 1c44ef2b11f42b28af188b6ac60a91c95102391f Mon Sep 17 00:00:00 2001 From: Levi Cameron Date: Tue, 30 May 2017 10:43:33 +1000 Subject: [PATCH 14/28] Improvements to code clarity --- rest_framework/static/rest_framework/js/ajax-form.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/static/rest_framework/js/ajax-form.js b/rest_framework/static/rest_framework/js/ajax-form.js index 88cb133bf..cb41343ee 100644 --- a/rest_framework/static/rest_framework/js/ajax-form.js +++ b/rest_framework/static/rest_framework/js/ajax-form.js @@ -40,8 +40,10 @@ function doAjaxSubmit(e) { if (contentType === 'multipart/form-data') { // We need to add a boundary parameter to the header + // We assume the first valid-looking boundary line in the body is correct + // regex is from RFC 2046 appendix A var re = /^--([0-9A-Z'()+_,-./:=?]{1,70})[ \f\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*$/im; - var boundary = re.exec(data); + var boundary = data.match(re); if (boundary !== null) { contentType += '; boundary="' + boundary[1] + '"'; } From 61158151082e4505cc6127a1c82103055f0e361f Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 30 May 2017 13:57:45 -0400 Subject: [PATCH 15/28] Special case for when OneToOneField is also primary key. https://github.com/encode/django-rest-framework/issues/5135 --- rest_framework/serializers.py | 5 +++ tests/models.py | 8 +++++ tests/test_relations_pk.py | 57 ++++++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e27610178..a4b51ae9d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1159,6 +1159,11 @@ class ModelSerializer(Serializer): field_class = field_mapping[model_field] field_kwargs = get_field_kwargs(field_name, model_field) + # Special case to handle when a OneToOneField is also the primary key + if model_field.one_to_one and model_field.primary_key: + field_class = self.serializer_related_field + field_kwargs['queryset'] = model_field.related_model.objects + if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. diff --git a/tests/models.py b/tests/models.py index 85143566e..6c9dde8fa 100644 --- a/tests/models.py +++ b/tests/models.py @@ -88,3 +88,11 @@ class NullableOneToOneSource(RESTFrameworkModel): target = models.OneToOneField( OneToOneTarget, null=True, blank=True, related_name='nullable_source', on_delete=models.CASCADE) + + +class OneToOnePKSource(RESTFrameworkModel): + """ Test model where the primary key is a OneToOneField with another model. """ + name = models.CharField(max_length=100) + target = models.OneToOneField( + OneToOneTarget, primary_key=True, + related_name='required_source', on_delete=models.CASCADE) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 8ccf0e117..69572581a 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -6,7 +6,7 @@ from django.utils import six from rest_framework import serializers from tests.models import ( ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget, - NullableForeignKeySource, NullableOneToOneSource, + NullableForeignKeySource, NullableOneToOneSource, OneToOnePKSource, NullableUUIDForeignKeySource, OneToOneTarget, UUIDForeignKeyTarget ) @@ -63,6 +63,13 @@ class NullableOneToOneTargetSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'nullable_source') +class OneToOnePKSourceSerializer(serializers.ModelSerializer): + + class Meta: + model = OneToOnePKSource + fields = '__all__' + + # TODO: Add test that .data cannot be accessed prior to .is_valid class PKManyToManyTests(TestCase): @@ -486,3 +493,51 @@ class PKNullableOneToOneTests(TestCase): {'id': 2, 'name': 'target-2', 'nullable_source': 1}, ] assert serializer.data == expected + + +class OneToOnePrimaryKeyTests(TestCase): + + def setUp(self): + # Given: Some target models already exist + self.target = target = OneToOneTarget(name='target-1') + target.save() + self.alt_target = alt_target = OneToOneTarget(name='target-2') + alt_target.save() + + def test_one_to_one_when_primary_key(self): + # When: Creating a Source pointing at the id of the second Target + target_pk = self.alt_target.id + source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk}) + # Then: The source is valid with the serializer + if not source.is_valid(): + self.fail("Expected OneToOnePKTargetSerializer to be valid but had errors: {}".format(source.errors)) + # Then: Saving the serializer creates a new object + new_source = source.save() + # Then: The new object has the same pk as the target object + self.assertEqual(new_source.pk, target_pk) + + def test_one_to_one_when_primary_key_no_duplicates(self): + # When: Creating a Source pointing at the id of the second Target + target_pk = self.target.id + data = {'name': 'source-1', 'target': target_pk} + source = OneToOnePKSourceSerializer(data=data) + # Then: The source is valid with the serializer + self.assertTrue(source.is_valid()) + # Then: Saving the serializer creates a new object + new_source = source.save() + # Then: The new object has the same pk as the target object + self.assertEqual(new_source.pk, target_pk) + # When: Trying to create a second object + second_source = OneToOnePKSourceSerializer(data=data) + self.assertFalse(second_source.is_valid()) + expected = {'target': [u'one to one pk source with this target already exists.']} + self.assertDictEqual(second_source.errors, expected) + + def test_one_to_one_when_primary_key_does_not_exist(self): + # Given: a target PK that does not exist + target_pk = self.target.pk + self.alt_target.pk + source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk}) + # Then: The source is not valid with the serializer + self.assertFalse(source.is_valid()) + self.assertIn("Invalid pk", source.errors['target'][0]) + self.assertIn("object does not exist", source.errors['target'][0]) From 88f9dbceecfaa08f68e5e60dc575e10a666de909 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 30 May 2017 14:13:29 -0400 Subject: [PATCH 16/28] Silly linting change import ordering matters --- tests/test_relations_pk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 69572581a..2eebe1b5c 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -6,8 +6,8 @@ from django.utils import six from rest_framework import serializers from tests.models import ( ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget, - NullableForeignKeySource, NullableOneToOneSource, OneToOnePKSource, - NullableUUIDForeignKeySource, OneToOneTarget, UUIDForeignKeyTarget + NullableForeignKeySource, NullableOneToOneSource, NullableUUIDForeignKeySource, + OneToOnePKSource, OneToOneTarget, UUIDForeignKeyTarget ) From 6b8d6019eaae72e93c02130c1794353994e08c7e Mon Sep 17 00:00:00 2001 From: Levi Cameron Date: Wed, 31 May 2017 13:12:06 +1000 Subject: [PATCH 17/28] Fix boundary detection regex not handling spaces inside boundary marker correctly --- rest_framework/static/rest_framework/js/ajax-form.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/static/rest_framework/js/ajax-form.js b/rest_framework/static/rest_framework/js/ajax-form.js index cb41343ee..fc00926f5 100644 --- a/rest_framework/static/rest_framework/js/ajax-form.js +++ b/rest_framework/static/rest_framework/js/ajax-form.js @@ -42,7 +42,9 @@ function doAjaxSubmit(e) { // We need to add a boundary parameter to the header // We assume the first valid-looking boundary line in the body is correct // regex is from RFC 2046 appendix A - var re = /^--([0-9A-Z'()+_,-./:=?]{1,70})[ \f\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*$/im; + var boundaryCharNoSpace = "[0-9A-Z'()+_,-./:=?"; + var boundaryChar = boundaryCharNoSpace + ' '; + var re = new RegExp('^--([' + boundaryChar + ']{0,69}[' + boundaryCharNoSpace + '])[\\s]*?$', 'im'); var boundary = data.match(re); if (boundary !== null) { contentType += '; boundary="' + boundary[1] + '"'; From e4ec1d744e19ff76ea8452233ff6b4db17b9e312 Mon Sep 17 00:00:00 2001 From: Matt Broach Date: Tue, 30 May 2017 23:39:18 -0400 Subject: [PATCH 18/28] Updated DjangoRestMulipleModel documentation to point to new repo location --- docs/api-guide/generic-views.md | 2 +- docs/topics/third-party-packages.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index ae2705080..0170256f2 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -390,4 +390,4 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements [UpdateModelMixin]: #updatemodelmixin [DestroyModelMixin]: #destroymodelmixin [django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk -[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels +[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels diff --git a/docs/topics/third-party-packages.md b/docs/topics/third-party-packages.md index 5d5fa3c68..44639f3d2 100644 --- a/docs/topics/third-party-packages.md +++ b/docs/topics/third-party-packages.md @@ -296,7 +296,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-compound-fields]: https://github.com/estebistec/drf-compound-fields [django-extra-fields]: https://github.com/Hipo/drf-extra-fields [djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk -[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels +[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db.rest]: http://wq.io/docs/about-rest [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack From 903ef4917a02ea53a8fb82fc52addb807fc106de Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 2 Jun 2017 09:48:06 +0200 Subject: [PATCH 19/28] Feat - Added aria-label and a new region for accessibility purpose Navigating the page with a reader is easier is there is aria-label and region. https://www.w3.org/WAI/ --- rest_framework/templates/rest_framework/base.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index bd87ba8a6..2587567d7 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -32,7 +32,8 @@
{% block navbar %} -