Repository: django-json-api/django-rest-framework-json-api

Issue:
retrieve_related fails for toOne relationship
@Anton-Shutik Using your new  #451 support for related links, using`{'get': 'retrieve_related'}` works fine for a serializer `ResourceRelatedField` where `many=True` but fails for a toOne relationship where `many=False`. 

Using the old style, I had to override `get_queryset()` for `many=True` views and `get_object()` for `many=False` views so I expect there is a similar need for another "flavor" of `retrieve_related` for toOne relationships. Does this make sense?

Here's my old view code for reference:

```python
class CourseTermViewSet(CourseBaseViewSet):
    queryset = CourseTerm.objects.all()
    serializer_class = CourseTermSerializer

    def get_queryset(self, *args, **kwargs):
        """
        Implement `related` view:
        Override `.list` if course_pk kwarg is present since course is toMany to course_term.
        """
        course_pk = self.kwargs.get('course_pk', None)
        if course_pk is not None:
            return self.queryset.filter(course_id=course_pk)
        return super(CourseTermViewSet, self).get_queryset()
```

```python
class CourseViewSet(CourseBaseViewSet):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

    def get_object(self):
        """
        Implement `related` view:
        Override `.retrieve` if course_term_pk kwarg is present since course_term is toOne to course.
        """
        course_term_pk = self.kwargs.get('course_term_pk', None)
        if course_term_pk is not None:
            return CourseTerm.objects.get(id=course_term_pk).course
        return super(CourseViewSet, self).get_object()
```

The new code (eliminates those overrides, sets the kwarg to `pk` and updates the `related_link_view_name`s to `course-related` and `course_term-related`, respectively and throws this error when retrieved:

```
AttributeError at /v1/course_terms/a1d34785-cc25-4c1c-9806-9d05a98068c7/course/
Got AttributeError when attempting to get a value for field `school_bulletin_prefix_code` on serializer `CourseSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `PKOnlyObject` instance.
Original exception text was: 'PKOnlyObject' object has no attribute 'school_bulletin_prefix_code'.

...
Traceback:

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/fields.py" in get_attribute
  441.             return get_attribute(instance, self.source_attrs)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/fields.py" in get_attribute
  100.                 instance = getattr(instance, attr)

During handling of the above exception ('PKOnlyObject' object has no attribute 'school_bulletin_prefix_code'), another exception occurred:

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
  34.             response = get_response(request)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  126.                 response = self.process_exception_by_middleware(e, request)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  124.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/views/decorators/csrf.py" in wrapped_view
  54.         return view_func(*args, **kwargs)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/viewsets.py" in view
  103.             return self.dispatch(request, *args, **kwargs)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/views.py" in dispatch
  483.             response = self.handle_exception(exc)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/views.py" in handle_exception
  443.             self.raise_uncaught_exception(exc)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/views.py" in dispatch
  480.             response = handler(request, *args, **kwargs)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework_json_api/views.py" in retrieve_related
  128.         return Response(serializer.data)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/serializers.py" in data
  560.         ret = super(Serializer, self).data

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/serializers.py" in data
  262.                 self._data = self.to_representation(self.instance)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/serializers.py" in to_representation
  514.                 attribute = field.get_attribute(instance)

File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/rest_framework/fields.py" in get_attribute
  462.             raise type(exc)(msg)

```


Discussion:
my old urls looked like this:
```python
    url(r'^v1/courses/(?P<course_pk>[^/.]+)/course_terms/',
        views.CourseTermViewSet.as_view({'get': 'list'}),
        name='course-course_terms'),
    url(r'^v1/course_terms/(?P<course_term_pk>[^/.]+)/course/',
        views.CourseViewSet.as_view({'get': 'retrieve'}), # a toOne relationship
        name='course_terms-course'),
```
and here are the models:
```
class Course(CommonModel):
    school_bulletin_prefix_code = models.CharField(max_length=10)
    suffix_two = models.CharField(max_length=2)
    subject_area_code = models.CharField(max_length=10)
    course_number = models.CharField(max_length=10)
    course_identifier = models.CharField(max_length=10, unique=True)
    course_name = models.CharField(max_length=80)
    course_description = models.TextField()

    class Meta:
        ordering = ["course_number"]

    def __str__(self):
        return '%s,%s,%s,%s' % (
            self.id,
            self.course_number,
            self.course_identifier,
            self.course_name
        )


class CourseTerm(CommonModel):
    term_identifier = models.TextField(max_length=10)
    audit_permitted_code = models.PositiveIntegerField(blank=True, default=0)
    exam_credit_flag = models.BooleanField(default=True)
    course = models.ForeignKey('myapp.Course', related_name='course_terms', on_delete=models.CASCADE, null=True,
                               default=None)

    class Meta:
        ordering = ["term_identifier"]

    def __str__(self):
        return '%s,%s,%s' % (self.id, self.term_identifier, self.course.course_identifier)
```
Here's where it happens but not sure what the fix is:
```
class RelatedMixin(object):
    """
    This mixin handles all related entities, whose Serializers are declared in "related_serializers"
    """

    def retrieve_related(self, request, *args, **kwargs):
        serializer_kwargs = {}
        instance = self.get_related_instance()

        returns PKOnlyObject instead of the full object.
--------
This is because ResourceRelatedField use_pk_only_optimization returns True.
---------

in rest_framework_json_api/views.py L160:

    def get_related_instance(self):
        parent_obj = self.get_object()
        parent_serializer = self.serializer_class(parent_obj)
        field_name = self.get_related_field_name()
        field = parent_serializer.fields.get(field_name, None)

        if field is not None:
            return field.get_attribute(parent_obj)
        else:
            try:
                return getattr(parent_obj, field_name)
            except AttributeError:
                raise NotFound

-------

in rest_framework/relations.py L162: self.use_pk_only_optimization() returns true from ResourceRelatedField.use_pk_only_optimization.

    def get_attribute(self, instance):
        if self.use_pk_only_optimization() and self.source_attrs:
            # Optimized case, return a mock object only containing the pk attribute.
            try:
                instance = get_attribute(instance, self.source_attrs[:-1])
                value = instance.serializable_value(self.source_attrs[-1])
                if is_simple_callable(value):
                    # Handle edge case where the relationship `source` argument
                    # points to a `get_relationship()` method on the model
                    value = value().pk
                return PKOnlyObject(pk=value)
            except AttributeError:
                pass


--------

in rest_framework_json_api/relations.py L197 ResourceRelatedField:

    def use_pk_only_optimization(self):
        # We need the real object to determine its type...
        return self.get_resource_type_from_included_serializer() is not None
```
This works around the problem:
```python
class RRF(ResourceRelatedField):
    def use_pk_only_optimization(self):
        return False
```
Looks like this commit: b4dffb8291d5c5ad7e9dae52a6942709082ff77a in #374

@santiavenda2 perhaps you can take a look.
```
commit b4dffb8291d5c5ad7e9dae52a6942709082ff77a
Author: santiavenda <santiavenda2@gmail.com>
Date:   Mon Jan 8 18:02:47 2018 -0800

    Avoid query explosion for related fields where I can user pk_only_opt… (#374)
    
    * Fix factory boy dependency
    
    * fix factory-boy version in setup.py and requirements-development.txt
    
    * Fix setup.py factory boy dependency
    
    * Avoid query explosion for related fields where I can user pk_only_optimization
    
    * Fix autoPrefetchMixin for ReverseOneToOneDescriptor
    
    * Fix code style
    
    * Avoid query objects in ModelSerializer to_representation method
    
    * Fix code queality error
    
    * Fix problem that makes None related objects not being renderer
    
    * Fix problem that makes None related objects not being renderer
    
    * _readable_field property is missing in drf<3.2
```
@n2ygk Hi, can you show how the fields are declared on your `CourseSerializer` ?
@Anton-Shutik It's here: https://github.com/django-json-api/django-rest-framework-json-api/blob/f02c7c46e09035cde8204d2f25c0c7a577519f9c/example/serializers.py#L333-L362
@Anton-Shutik and see also here for the test case: https://github.com/django-json-api/django-rest-framework-json-api/blob/f02c7c46e09035cde8204d2f25c0c7a577519f9c/example/tests/test_views.py#L330-L355

Resolving patch:
diff --git a/example/serializers.py b/example/serializers.py
--- a/example/serializers.py
+++ b/example/serializers.py
@@ -225,6 +225,7 @@ class AuthorSerializer(serializers.ModelSerializer):
     }
     related_serializers = {
         'bio': 'example.serializers.AuthorBioSerializer',
+        'type': 'example.serializers.AuthorTypeSerializer',
         'entries': 'example.serializers.EntrySerializer',
         'first_entry': 'example.serializers.EntrySerializer'
     }
diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py
--- a/rest_framework_json_api/views.py
+++ b/rest_framework_json_api/views.py
@@ -14,6 +14,8 @@
 from django.utils.module_loading import import_string as import_class_from_dotted_path
 from rest_framework import generics, viewsets
 from rest_framework.exceptions import MethodNotAllowed, NotFound
+from rest_framework.fields import get_attribute
+from rest_framework.relations import PKOnlyObject
 from rest_framework.response import Response
 from rest_framework.reverse import reverse
 from rest_framework.serializers import Serializer
@@ -164,7 +166,11 @@ def get_related_instance(self):
         field = parent_serializer.fields.get(field_name, None)
 
         if field is not None:
-            return field.get_attribute(parent_obj)
+            instance = field.get_attribute(parent_obj)
+            if isinstance(instance, PKOnlyObject):
+                # need whole object
+                instance = get_attribute(parent_obj, field.source_attrs)
+            return instance
         else:
             try:
                 return getattr(parent_obj, field_name)


Test patch:
diff --git a/example/tests/test_views.py b/example/tests/test_views.py
--- a/example/tests/test_views.py
+++ b/example/tests/test_views.py
@@ -325,11 +325,11 @@ def test_get_serializer_comes_from_included_serializers(self):
         view.serializer_class.related_serializers = related_serializers
 
     def test_get_serializer_class_raises_error(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'type'}
+        kwargs = {'pk': self.author.id, 'related_field': 'unknown'}
         view = self._get_view(kwargs)
         self.assertRaises(NotFound, view.get_serializer_class)
 
-    def test_retrieve_related_single(self):
+    def test_retrieve_related_single_reverse_lookup(self):
         url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'bio'})
         resp = self.client.get(url)
         expected = {
@@ -345,6 +345,20 @@ def test_retrieve_related_single(self):
         self.assertEqual(resp.status_code, 200)
         self.assertEqual(resp.json(), expected)
 
+    def test_retrieve_related_single(self):
+        url = reverse('author-related', kwargs={'pk': self.author.type.pk, 'related_field': 'type'})
+        resp = self.client.get(url)
+        expected = {
+            'data': {
+                'type': 'authorTypes', 'id': str(self.author.type.id),
+                'attributes': {
+                    'name': str(self.author.type.name)
+                },
+            }
+        }
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(resp.json(), expected)
+
     def test_retrieve_related_many(self):
         entry = EntryFactory(authors=self.author)
         url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'entries'})