diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 741943de8..8a810edb9 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -116,6 +116,8 @@ By default this field is read-write, although you can change this behavior using * `queryset` - The queryset used for model instance lookups when validating the field input. Relationships must either set a queryset explicitly, or set `read_only=True`. * `many` - If applied to a to-many relationship, you should set this argument to `True`. * `allow_null` - If set to `True`, the field will accept values of `None` or the empty string for nullable relationships. Defaults to `False`. +* `pk_field` - Set to a field to control serialization/deserialization of the primary key's value. For example, `pk_field=UUIDField(format='hex')` would serialize a UUID primary key into its compact hex representation. + ## HyperlinkedRelatedField diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 00a4a2656..4f81dde34 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -134,10 +134,16 @@ class PrimaryKeyRelatedField(RelatedField): 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), } + def __init__(self, **kwargs): + self.pk_field = kwargs.pop('pk_field', None) + super(PrimaryKeyRelatedField, self).__init__(**kwargs) + def use_pk_only_optimization(self): return True def to_internal_value(self, data): + if self.pk_field is not None: + data = self.pk_field.to_internal_value(data) try: return self.get_queryset().get(pk=data) except ObjectDoesNotExist: @@ -146,6 +152,8 @@ class PrimaryKeyRelatedField(RelatedField): self.fail('incorrect_type', data_type=type(data).__name__) def to_representation(self, value): + if self.pk_field is not None: + return self.pk_field.to_representation(value.pk) return value.pk diff --git a/tests/test_relations.py b/tests/test_relations.py index fbe176e24..a4815a06d 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,3 +1,4 @@ +import uuid from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset from django.core.exceptions import ImproperlyConfigured from django.utils.datastructures import MultiValueDict @@ -48,6 +49,34 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk +class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase): + def setUp(self): + self.queryset = MockQueryset([ + MockObject(pk=uuid.UUID(int=0), name='foo'), + MockObject(pk=uuid.UUID(int=1), name='bar'), + MockObject(pk=uuid.UUID(int=2), name='baz') + ]) + self.instance = self.queryset.items[2] + self.field = serializers.PrimaryKeyRelatedField( + queryset=self.queryset, + pk_field=serializers.UUIDField(format='int') + ) + + def test_pk_related_lookup_exists(self): + instance = self.field.to_internal_value(self.instance.pk.int) + assert instance is self.instance + + def test_pk_related_lookup_does_not_exist(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.field.to_internal_value(4) + msg = excinfo.value.detail[0] + assert msg == 'Invalid pk "00000000-0000-0000-0000-000000000004" - object does not exist.' + + def test_pk_representation(self): + representation = self.field.to_representation(self.instance) + assert representation == self.instance.pk.int + + class TestHyperlinkedIdentityField(APISimpleTestCase): def setUp(self): self.instance = MockObject(pk=1, name='foo')