Skip to content

Commit 8fa3284

Browse files
committed
Merge remote-tracking branch 'reference/master'
2 parents d79956d + 7f6fb31 commit 8fa3284

File tree

7 files changed

+163
-14
lines changed

7 files changed

+163
-14
lines changed

docs/api-guide/authentication.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403
247247

248248
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
249249

250+
**Warning**: Always use Django's standard login view when creating login pages. This will ensure your login views are properly protected.
251+
252+
CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied.
253+
250254
# Custom authentication
251255

252256
To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.

docs/api-guide/fields.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,18 @@ Corresponds to `django.db.models.fields.TimeField`
302302

303303
Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
304304

305+
## DurationField
306+
307+
A Duration representation.
308+
Corresponds to `django.db.models.fields.DurationField`
309+
310+
The `validated_data` for these fields will contain a `datetime.timedelta` instance.
311+
The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu]'`.
312+
313+
**Note:** This field is only available with Django versions >= 1.8.
314+
315+
**Signature:** `DurationField()`
316+
305317
---
306318

307319
# Choice selection fields

rest_framework/compat.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,11 @@ def apply_markdown(text):
258258
SHORT_SEPARATORS = (b',', b':')
259259
LONG_SEPARATORS = (b', ', b': ')
260260
INDENT_SEPARATORS = (b',', b': ')
261+
262+
263+
if django.VERSION >= (1, 8):
264+
from django.db.models import DurationField
265+
from django.utils.dateparse import parse_duration
266+
from django.utils.duration import duration_string
267+
else:
268+
DurationField = duration_string = parse_duration = None

rest_framework/fields.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from rest_framework.compat import (
1313
EmailValidator, MinValueValidator, MaxValueValidator,
1414
MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict,
15-
unicode_repr, unicode_to_repr
15+
unicode_repr, unicode_to_repr, parse_duration, duration_string,
1616
)
1717
from rest_framework.exceptions import ValidationError
1818
from rest_framework.settings import api_settings
@@ -1003,6 +1003,29 @@ def to_representation(self, value):
10031003
return value.strftime(self.format)
10041004

10051005

1006+
class DurationField(Field):
1007+
default_error_messages = {
1008+
'invalid': _('Duration has wrong format. Use one of these formats instead: {format}.'),
1009+
}
1010+
1011+
def __init__(self, *args, **kwargs):
1012+
if parse_duration is None:
1013+
raise NotImplementedError(
1014+
'DurationField not supported for django versions prior to 1.8')
1015+
return super(DurationField, self).__init__(*args, **kwargs)
1016+
1017+
def to_internal_value(self, value):
1018+
if isinstance(value, datetime.timedelta):
1019+
return value
1020+
parsed = parse_duration(value)
1021+
if parsed is not None:
1022+
return parsed
1023+
self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]')
1024+
1025+
def to_representation(self, value):
1026+
return duration_string(value)
1027+
1028+
10061029
# Choice types...
10071030

10081031
class ChoiceField(Field):
@@ -1060,7 +1083,11 @@ def get_value(self, dictionary):
10601083
# We override the default field access in order to support
10611084
# lists in HTML forms.
10621085
if html.is_html_input(dictionary):
1063-
return dictionary.getlist(self.field_name)
1086+
ret = dictionary.getlist(self.field_name)
1087+
if getattr(self.root, 'partial', False) and not ret:
1088+
ret = empty
1089+
return ret
1090+
10641091
return dictionary.get(self.field_name, empty)
10651092

10661093
def to_internal_value(self, data):

rest_framework/serializers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
1616
from django.db.models import query
1717
from django.utils.translation import ugettext_lazy as _
18-
from rest_framework.compat import postgres_fields, unicode_to_repr
18+
from rest_framework.compat import (
19+
postgres_fields,
20+
unicode_to_repr,
21+
DurationField as ModelDurationField,
22+
)
1923
from rest_framework.utils import model_meta
2024
from rest_framework.utils.field_mapping import (
2125
get_url_kwargs, get_field_kwargs,
@@ -731,6 +735,8 @@ class ModelSerializer(Serializer):
731735
models.TimeField: TimeField,
732736
models.URLField: URLField,
733737
}
738+
if ModelDurationField is not None:
739+
serializer_field_mapping[ModelDurationField] = DurationField
734740
serializer_related_field = PrimaryKeyRelatedField
735741
serializer_url_field = HyperlinkedIdentityField
736742
serializer_choice_field = ChoiceField
@@ -1088,6 +1094,9 @@ def include_extra_kwargs(self, kwargs, extra_kwargs):
10881094
if extra_kwargs.get('default') and kwargs.get('required') is False:
10891095
kwargs.pop('required')
10901096

1097+
if kwargs.get('read_only', False):
1098+
extra_kwargs.pop('required', None)
1099+
10911100
kwargs.update(extra_kwargs)
10921101

10931102
return kwargs

tests/test_fields.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from decimal import Decimal
22
from django.utils import timezone
33
from rest_framework import serializers
4+
import rest_framework
45
import datetime
56
import django
67
import pytest
@@ -221,6 +222,14 @@ def test_invalid_error_key(self):
221222
assert str(exc_info.value) == expected
222223

223224

225+
class MockHTMLDict(dict):
226+
"""
227+
This class mocks up a dictionary like object, that behaves
228+
as if it was returned for multipart or urlencoded data.
229+
"""
230+
getlist = None
231+
232+
224233
class TestBooleanHTMLInput:
225234
def setup(self):
226235
class TestSerializer(serializers.Serializer):
@@ -234,21 +243,11 @@ def test_empty_html_checkbox(self):
234243
"""
235244
# This class mocks up a dictionary like object, that behaves
236245
# as if it was returned for multipart or urlencoded data.
237-
class MockHTMLDict(dict):
238-
getlist = None
239246
serializer = self.Serializer(data=MockHTMLDict())
240247
assert serializer.is_valid()
241248
assert serializer.validated_data == {'archived': False}
242249

243250

244-
class MockHTMLDict(dict):
245-
"""
246-
This class mocks up a dictionary like object, that behaves
247-
as if it was returned for multipart or urlencoded data.
248-
"""
249-
getlist = None
250-
251-
252251
class TestHTMLInput:
253252
def test_empty_html_charfield(self):
254253
class TestSerializer(serializers.Serializer):
@@ -905,6 +904,29 @@ class TestNoOutputFormatTimeField(FieldValues):
905904
field = serializers.TimeField(format=None)
906905

907906

907+
@pytest.mark.skipif(django.VERSION < (1, 8),
908+
reason='DurationField is only available for django1.8+')
909+
class TestDurationField(FieldValues):
910+
"""
911+
Valid and invalid values for `DurationField`.
912+
"""
913+
valid_inputs = {
914+
'13': datetime.timedelta(seconds=13),
915+
'3 08:32:01.000123': datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123),
916+
'08:01': datetime.timedelta(minutes=8, seconds=1),
917+
datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123),
918+
}
919+
invalid_inputs = {
920+
'abc': ['Duration has wrong format. Use one of these formats instead: [DD] [HH:[MM:]]ss[.uuuuuu].'],
921+
'3 08:32 01.123': ['Duration has wrong format. Use one of these formats instead: [DD] [HH:[MM:]]ss[.uuuuuu].'],
922+
}
923+
outputs = {
924+
datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): '3 08:32:01.000123',
925+
}
926+
if django.VERSION >= (1, 8):
927+
field = serializers.DurationField()
928+
929+
908930
# Choice types...
909931

910932
class TestChoiceField(FieldValues):
@@ -1017,6 +1039,15 @@ class TestMultipleChoiceField(FieldValues):
10171039
]
10181040
)
10191041

1042+
def test_against_partial_and_full_updates(self):
1043+
# serializer = self.Serializer(data=MockHTMLDict())
1044+
from django.http import QueryDict
1045+
field = serializers.MultipleChoiceField(choices=(('a', 'a'), ('b', 'b')))
1046+
field.partial = False
1047+
assert field.get_value(QueryDict({})) == []
1048+
field.partial = True
1049+
assert field.get_value(QueryDict({})) == rest_framework.fields.empty
1050+
10201051

10211052
# File serializers...
10221053

tests/test_model_serializer.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
an appropriate set of serializer fields for each case.
77
"""
88
from __future__ import unicode_literals
9+
import django
910
from django.core.exceptions import ImproperlyConfigured
1011
from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
1112
from django.db import models
1213
from django.test import TestCase
1314
from django.utils import six
15+
import pytest
1416
from rest_framework import serializers
15-
from rest_framework.compat import unicode_repr
17+
from rest_framework.compat import unicode_repr, DurationField as ModelDurationField
1618

1719

1820
def dedent(blocktext):
@@ -284,6 +286,28 @@ class Meta:
284286
ChildSerializer().fields
285287

286288

289+
@pytest.mark.skipif(django.VERSION < (1, 8),
290+
reason='DurationField is only available for django1.8+')
291+
class TestDurationFieldMapping(TestCase):
292+
def test_duration_field(self):
293+
class DurationFieldModel(models.Model):
294+
"""
295+
A model that defines DurationField.
296+
"""
297+
duration_field = ModelDurationField()
298+
299+
class TestSerializer(serializers.ModelSerializer):
300+
class Meta:
301+
model = DurationFieldModel
302+
303+
expected = dedent("""
304+
TestSerializer():
305+
id = IntegerField(label='ID', read_only=True)
306+
duration_field = DurationField()
307+
""")
308+
self.assertEqual(unicode_repr(TestSerializer()), expected)
309+
310+
287311
# Tests for relational field mappings.
288312
# ------------------------------------
289313

@@ -316,6 +340,14 @@ class RelationalModel(models.Model):
316340
through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
317341

318342

343+
class UniqueTogetherModel(models.Model):
344+
foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='unique_foreign_key')
345+
one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='unique_one_to_one')
346+
347+
class Meta:
348+
unique_together = ("foreign_key", "one_to_one")
349+
350+
319351
class TestRelationalFieldMappings(TestCase):
320352
def test_pk_relations(self):
321353
class TestSerializer(serializers.ModelSerializer):
@@ -395,6 +427,32 @@ class Meta:
395427
""")
396428
self.assertEqual(unicode_repr(TestSerializer()), expected)
397429

430+
def test_nested_unique_together_relations(self):
431+
class TestSerializer(serializers.HyperlinkedModelSerializer):
432+
class Meta:
433+
model = UniqueTogetherModel
434+
depth = 1
435+
expected = dedent("""
436+
TestSerializer():
437+
url = HyperlinkedIdentityField(view_name='uniquetogethermodel-detail')
438+
foreign_key = NestedSerializer(read_only=True):
439+
url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
440+
name = CharField(max_length=100)
441+
one_to_one = NestedSerializer(read_only=True):
442+
url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
443+
name = CharField(max_length=100)
444+
class Meta:
445+
validators = [<UniqueTogetherValidator(queryset=UniqueTogetherModel.objects.all(), fields=('foreign_key', 'one_to_one'))>]
446+
""")
447+
if six.PY2:
448+
# This case is also too awkward to resolve fully across both py2
449+
# and py3. (See above)
450+
expected = expected.replace(
451+
"('foreign_key', 'one_to_one')",
452+
"(u'foreign_key', u'one_to_one')"
453+
)
454+
self.assertEqual(unicode_repr(TestSerializer()), expected)
455+
398456
def test_pk_reverse_foreign_key(self):
399457
class TestSerializer(serializers.ModelSerializer):
400458
class Meta:

0 commit comments

Comments
 (0)