{% block content %}
+
{% if 'GET' in allowed_methods %}
-
+
@@ -148,11 +151,11 @@
{% endif %}
-
{{ name }}
+
-
{{ request.method }} {{ request.get_full_path }}
+
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}{% for key, val in response_headers|items %}
{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}{% endfor %}
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
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_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=)
diff --git a/tests/test_relations.py b/tests/test_relations.py
index c903ee557..fd3256e89 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([
@@ -96,7 +152,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 +172,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):
diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py
index 8ccf0e117..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,
- NullableUUIDForeignKeySource, OneToOneTarget, UUIDForeignKeyTarget
+ NullableForeignKeySource, NullableOneToOneSource, NullableUUIDForeignKeySource,
+ OneToOnePKSource, 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])
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}
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([