From 067360c4ee721a22542e23310d42cf0a47b89991 Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Mon, 19 Jan 2015 14:55:35 +0100 Subject: [PATCH 01/10] Added support for the to_field on ForeignKey Fields. Uses the SlugRelatedField instead. SlugRelatedField will only get the Slugfield from the ORM as it does not need the other stuff --- rest_framework/relations.py | 2 +- rest_framework/serializers.py | 10 +++++++++- rest_framework/utils/field_mapping.py | 5 ++++- rest_framework/utils/model_meta.py | 6 ++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index aa0c2defe7..3a081fa653 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -303,7 +303,7 @@ def __init__(self, slug_field=None, **kwargs): def to_internal_value(self, data): try: - return self.get_queryset().get(**{self.slug_field: data}) + return self.get_queryset().only(self.slug_field).get(**{self.slug_field: data}) except ObjectDoesNotExist: self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data)) except (TypeError, ValueError): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e373cd107a..c7c751f110 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -727,6 +727,7 @@ class ModelSerializer(Serializer): models.URLField: URLField, }) _related_class = PrimaryKeyRelatedField + _related_to_field_class = SlugRelatedField def create(self, validated_data): """ @@ -1002,8 +1003,15 @@ def get_fields(self): field_cls = self._get_nested_class(depth, relation_info) kwargs = get_nested_relation_kwargs(relation_info) else: - field_cls = self._related_class kwargs = get_relation_kwargs(field_name, relation_info) + to_field = kwargs.get('to_field', False) + if to_field: + # using the slug field for now + kwargs.pop('to_field', None) + kwargs['slug_field'] = to_field + field_cls = self._related_to_field_class + else: + field_cls = self._related_class # `view_name` is only valid for hyperlinked relationships. if not issubclass(field_cls, HyperlinkedRelatedField): kwargs.pop('view_name', None) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index cba40d3184..b48f58c028 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -194,7 +194,7 @@ def get_relation_kwargs(field_name, relation_info): """ Creates a default instance of a flat relational field. """ - model_field, related_model, to_many, has_through_model = relation_info + model_field, related_model, to_many, to_field, has_through_model = relation_info kwargs = { 'queryset': related_model._default_manager, 'view_name': get_detail_view_name(related_model) @@ -203,6 +203,9 @@ def get_relation_kwargs(field_name, relation_info): if to_many: kwargs['many'] = True + if to_field: + kwargs['to_field'] = to_field + if has_through_model: kwargs['read_only'] = True kwargs.pop('queryset', None) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 375d2e8c60..0f57d14f07 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -26,6 +26,7 @@ 'model_field', 'related', 'to_many', + 'to_field', 'has_through_model' ]) @@ -96,10 +97,13 @@ def _get_forward_relationships(opts): """ forward_relations = OrderedDict() for field in [field for field in opts.fields if field.serialize and field.rel]: + forward_relations[field.name] = RelationInfo( model_field=field, related=_resolve_model(field.rel.to), to_many=False, + # to_fields is an array but django lets you only set one to_field + to_field=field.to_fields[0] if len(field.to_fields) else None, has_through_model=False ) @@ -109,6 +113,8 @@ def _get_forward_relationships(opts): model_field=field, related=_resolve_model(field.rel.to), to_many=True, + # to_fields is an array but django lets you only set one to_field + to_field=field.to_fields[0] if len(field.to_fields) else None, has_through_model=( not field.rel.through._meta.auto_created ) From 8263048af44bc1f3cf10ce740ee87e67b8a2a99a Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Mon, 19 Jan 2015 15:25:35 +0100 Subject: [PATCH 02/10] * many to many relationship do not have to_fields --- rest_framework/utils/model_meta.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 0f57d14f07..5affa5ba70 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -113,8 +113,8 @@ def _get_forward_relationships(opts): model_field=field, related=_resolve_model(field.rel.to), to_many=True, - # to_fields is an array but django lets you only set one to_field - to_field=field.to_fields[0] if len(field.to_fields) else None, + # manytomany do not have to_fields + to_field=None, has_through_model=( not field.rel.through._meta.auto_created ) @@ -134,6 +134,7 @@ def _get_reverse_relationships(opts): model_field=None, related=relation.model, to_many=relation.field.rel.multiple, + to_field=relation.field.to_fields[0] if len(relation.field.to_fields) else None, has_through_model=False ) @@ -144,6 +145,8 @@ def _get_reverse_relationships(opts): model_field=None, related=relation.model, to_many=True, + # manytomany do not have to_fields + to_field=None, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) and not relation.field.rel.through._meta.auto_created From d47f29a207b5e8dc8d6be32b343fae379003b49d Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Mon, 19 Jan 2015 16:00:21 +0100 Subject: [PATCH 03/10] Fixed the rest of the test cases. # Some tests do not have the full QuerySet API # But we optimize SlugRelatedFields by only loading what we need MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # it seems that some tests/django initializers are setting # to_field where it is totally unnecessary so SlugRelatedField was used… Added checks --- rest_framework/relations.py | 9 ++++++++- rest_framework/serializers.py | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 3a081fa653..b1a7eca65e 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -303,7 +303,14 @@ def __init__(self, slug_field=None, **kwargs): def to_internal_value(self, data): try: - return self.get_queryset().only(self.slug_field).get(**{self.slug_field: data}) + # Some tests do not have the full QuerySet API + # But we optimize SlugRelatedFields by only loading what we need + qs = self.get_queryset() + if hasattr(qs, 'only'): + return self.get_queryset().only(self.slug_field).get(**{self.slug_field: data}) + else: + return self.get_queryset().get(**{self.slug_field: data}) + except ObjectDoesNotExist: self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data)) except (TypeError, ValueError): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c7c751f110..9fc7d87a21 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1005,7 +1005,10 @@ def get_fields(self): else: kwargs = get_relation_kwargs(field_name, relation_info) to_field = kwargs.get('to_field', False) - if to_field: + kwargs.pop('to_field', None) + # it seems that some tests/django initializers are setting + # to_field where it is totally unnecessary + if to_field and to_field != 'id': # using the slug field for now kwargs.pop('to_field', None) kwargs['slug_field'] = to_field From 49ecaa7d9e727dfe0a0deaf95fd87298d40ee35f Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Mon, 19 Jan 2015 16:33:34 +0100 Subject: [PATCH 04/10] Add support for Django <1.6 where appearently no to_field existed --- rest_framework/utils/model_meta.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 5affa5ba70..caa93d4af6 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -97,13 +97,18 @@ def _get_forward_relationships(opts): """ forward_relations = OrderedDict() for field in [field for field in opts.fields if field.serialize and field.rel]: + # For < django 1.6 + if hasattr(field, 'to_fields'): + to_field = field.to_fields[0] if len(field.to_fields) else None + else: + to_field = None forward_relations[field.name] = RelationInfo( model_field=field, related=_resolve_model(field.rel.to), to_many=False, # to_fields is an array but django lets you only set one to_field - to_field=field.to_fields[0] if len(field.to_fields) else None, + to_field=to_field, has_through_model=False ) @@ -130,11 +135,17 @@ def _get_reverse_relationships(opts): reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() + # For < django 1.6 + if hasattr(relation.field, 'to_fields'): + to_field = relation.field.to_fields[0] if len(relation.field.to_fields) else None + else: + to_field = None + reverse_relations[accessor_name] = RelationInfo( model_field=None, related=relation.model, to_many=relation.field.rel.multiple, - to_field=relation.field.to_fields[0] if len(relation.field.to_fields) else None, + to_field=to_field, has_through_model=False ) From 6369f93d041e1879ca8ef05f0fd8fe4875fc9360 Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Mon, 19 Jan 2015 16:42:54 +0100 Subject: [PATCH 05/10] * Removed 2 unnecessary lines --- rest_framework/serializers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9fc7d87a21..2e31faa8f3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1004,13 +1004,11 @@ def get_fields(self): kwargs = get_nested_relation_kwargs(relation_info) else: kwargs = get_relation_kwargs(field_name, relation_info) - to_field = kwargs.get('to_field', False) - kwargs.pop('to_field', None) + to_field = kwargs.pop('to_field', None) # it seems that some tests/django initializers are setting # to_field where it is totally unnecessary if to_field and to_field != 'id': # using the slug field for now - kwargs.pop('to_field', None) kwargs['slug_field'] = to_field field_cls = self._related_to_field_class else: From f808b6c6bc8c78df5bed5ddeb1556c21fc09bd5b Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Tue, 20 Jan 2015 21:40:23 +0100 Subject: [PATCH 06/10] * Removed SlugRelatedField optimization * Moved compatibility check of to_fields to compat.py * Expanded the check with a way that should work on django <1.6. Need to check trevis after pushing.. --- rest_framework/compat.py | 15 +++++++++++++++ rest_framework/relations.py | 9 +-------- rest_framework/utils/model_meta.py | 18 +++--------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 971dee9cf5..cf820af42e 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -138,6 +138,21 @@ def __init__(self, *args, **kwargs): super(MaxLengthValidator, self).__init__(*args, **kwargs) +# ForeignKey's to_fields parameter was changed in Django 1.6 to an array named to_fields +# to_fields is an array but django lets you only set one to_field for ForeignKeys +# https://docs.djangoproject.com/en/1.7/ref/models/fields/#django.db.models.ForeignKey.to_field +# see also ForeignKey and ForeignObject in +# https://docs.djangoproject.com/en/1.7/_modules/django/db/models/fields/related/ + +if django.VERSION >= (1, 6): + def get_to_field(field): + # This should work for casual ForeignKeys + return field.to_fields[0] if len(field.to_fields) else None +else: + def get_to_field(field): + return field.to_field + + # URLValidator only accepts `message` in 1.6+ if django.VERSION >= (1, 6): from django.core.validators import URLValidator diff --git a/rest_framework/relations.py b/rest_framework/relations.py index b1a7eca65e..3a081fa653 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -303,14 +303,7 @@ def __init__(self, slug_field=None, **kwargs): def to_internal_value(self, data): try: - # Some tests do not have the full QuerySet API - # But we optimize SlugRelatedFields by only loading what we need - qs = self.get_queryset() - if hasattr(qs, 'only'): - return self.get_queryset().only(self.slug_field).get(**{self.slug_field: data}) - else: - return self.get_queryset().get(**{self.slug_field: data}) - + return self.get_queryset().only(self.slug_field).get(**{self.slug_field: data}) except ObjectDoesNotExist: self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data)) except (TypeError, ValueError): diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index caa93d4af6..e088fe2a9d 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -9,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils import six +from rest_framework.compat import get_to_field from rest_framework.compat import OrderedDict import inspect @@ -97,18 +98,11 @@ def _get_forward_relationships(opts): """ forward_relations = OrderedDict() for field in [field for field in opts.fields if field.serialize and field.rel]: - # For < django 1.6 - if hasattr(field, 'to_fields'): - to_field = field.to_fields[0] if len(field.to_fields) else None - else: - to_field = None - forward_relations[field.name] = RelationInfo( model_field=field, related=_resolve_model(field.rel.to), to_many=False, - # to_fields is an array but django lets you only set one to_field - to_field=to_field, + to_field=get_to_field(field), has_through_model=False ) @@ -135,17 +129,11 @@ def _get_reverse_relationships(opts): reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() - # For < django 1.6 - if hasattr(relation.field, 'to_fields'): - to_field = relation.field.to_fields[0] if len(relation.field.to_fields) else None - else: - to_field = None - reverse_relations[accessor_name] = RelationInfo( model_field=None, related=relation.model, to_many=relation.field.rel.multiple, - to_field=to_field, + to_field=get_to_field(relation.field), has_through_model=False ) From 6ccc45ee47348f6e59476edf89651dab2be427b1 Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Tue, 20 Jan 2015 21:54:16 +0100 Subject: [PATCH 07/10] Ups. Removed the wrong line. (optimizer) --- rest_framework/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 3a081fa653..aa0c2defe7 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -303,7 +303,7 @@ def __init__(self, slug_field=None, **kwargs): def to_internal_value(self, data): try: - return self.get_queryset().only(self.slug_field).get(**{self.slug_field: data}) + return self.get_queryset().get(**{self.slug_field: data}) except ObjectDoesNotExist: self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data)) except (TypeError, ValueError): From 64c49f8c47f7ea0578447b1a4ba36909ecfda2d0 Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Tue, 20 Jan 2015 22:17:28 +0100 Subject: [PATCH 08/10] =?UTF-8?q?From=20this=20i=20believe=20it=20is=20fie?= =?UTF-8?q?ld=5Fname=20pre=20django=201.6.=20I=20still=20don=E2=80=99t=20h?= =?UTF-8?q?ave=20django=201.5=20installed=20so=20it=20is=20an=20educated?= =?UTF-8?q?=20guess=20by=20looking=20at=20ManyToOneRel=20and=20ForeignKey?= =?UTF-8?q?=20https://github.com/django/django/blob/stable/1.5.x/django/db?= =?UTF-8?q?/models/fields/related.py#L915?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index a1b25f8196..82ad0ea933 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -158,7 +158,7 @@ def get_to_field(field): return field.to_fields[0] if len(field.to_fields) else None else: def get_to_field(field): - return field.to_field + return field.field_name # URLValidator only accepts `message` in 1.6+ From a496c523a453ec80001073b95b3e24d8c5ce12d7 Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Tue, 20 Jan 2015 22:32:58 +0100 Subject: [PATCH 09/10] to_field is rel.field_name at <1.6. love pdb and pip! --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 82ad0ea933..ca99f19e1e 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -158,7 +158,7 @@ def get_to_field(field): return field.to_fields[0] if len(field.to_fields) else None else: def get_to_field(field): - return field.field_name + return field.rel.field_name # URLValidator only accepts `message` in 1.6+ From 5ca98419c11814bb0eb8d699e90f9e85cf12f7db Mon Sep 17 00:00:00 2001 From: Tom Jaster Date: Thu, 16 Apr 2015 17:06:00 +0200 Subject: [PATCH 10/10] * updated patch to latest DRF revision --- rest_framework/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2eef6eeb51..c98ced5242 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -732,6 +732,7 @@ class ModelSerializer(Serializer): models.URLField: URLField, } serializer_related_field = PrimaryKeyRelatedField + serializer_related_to_field = SlugRelatedField serializer_url_field = HyperlinkedIdentityField serializer_choice_field = ChoiceField @@ -1021,6 +1022,11 @@ def build_relational_field(self, field_name, relation_info): field_class = self.serializer_related_field field_kwargs = get_relation_kwargs(field_name, relation_info) + to_field = field_kwargs.pop('to_field', None) + if to_field and to_field != 'id': + field_kwargs['slug_field'] = to_field + field_class = self.serializer_related_to_field + # `view_name` is only valid for hyperlinked relationships. if not issubclass(field_class, HyperlinkedRelatedField): field_kwargs.pop('view_name', None)