Skip to content

PKOnlyObject optimization may break HyperlinkedRelatedField #4653

Closed
@sebastian-philipp

Description

@sebastian-philipp

Checklist

  • I have verified that that issue exists against the master branch of Django REST framework.
  • I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
  • This is not a usage question. (Those should be directed to the discussion group instead.)
  • This cannot be dealt with as a third party library. (We prefer new functionality to be in the form of third party libraries where possible.)
  • I have reduced the issue to the simplest possible case.
  • I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)
    I've a mystery here. This is the Traceback I'm getting:
Internal Server Error: {u'kwargs': {}, u'args': (), u'request': <rest_framework.request.Request object at 0x7fe51daaf8d0>, u'view': <volumes.restapi.VolumeProxyViewSet object at 0x7fe51daaf890>}
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/rest_framework/views.py", line 463, in dispatch
    response = handler(request, *args, **kwargs)
  File "/usr/share/openattic/rest/multinode/handlers.py", line 101, in create
    return super(RequestHandlers, self).create(request, args, kwargs)
  File "/usr/share/openattic/volumes/restapi.py", line 447, in create
    return Response(serializer.data, status=status.HTTP_201_CREATED)
  File "/usr/lib/python2.7/dist-packages/rest_framework/serializers.py", line 503, in data
    ret = super(Serializer, self).data
  File "/usr/lib/python2.7/dist-packages/rest_framework/serializers.py", line 239, in data
    self._data = self.to_representation(self.instance)
  File "/usr/share/openattic/rest/utilities.py", line 102, in to_representation
    return self.to_native(instance)
  File "/usr/share/openattic/volumes/restapi.py", line 331, in to_native
    data.update(dict([(key, value) for (key, value) in serializer_instance.data.items()
  File "/usr/lib/python2.7/dist-packages/rest_framework/serializers.py", line 503, in data
    ret = super(Serializer, self).data
  File "/usr/lib/python2.7/dist-packages/rest_framework/serializers.py", line 239, in data
    self._data = self.to_representation(self.instance)
  File "/usr/lib/python2.7/dist-packages/rest_framework/serializers.py", line 472, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/usr/lib/python2.7/dist-packages/rest_framework/relations.py", line 355, in to_representation
    raise ImproperlyConfigured(msg % self.view_name)
ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "host-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.

This is the minimal testcase:

from django.db import models
from rest_framework import viewsets, serializers, relations

class F(models.Model):
    pass

class M(models.Model):
    @property
    def f(self):
        return F.objects.get()

class FSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = F
        fields = ('id', )

class MSerializer(serializers.HyperlinkedModelSerializer):
    f = relations.HyperlinkedRelatedField(read_only=True, view_name='f-detail')

    class Meta:
        model = M
        fields = ('id', 'f')

class FViewSet(viewsets.ModelViewSet):
    queryset = F.objects.all()
    serializer_class = FSerializer


class MViewSet(viewsets.ModelViewSet):
    queryset = M.objects.all()
    serializer_class = MSerializer

There is a problem, if

  1. You are using DRF >= 3.0
  2. and your serializer contains a HyperlinkedRelatedField named f
  3. and the corresponding model does not have a related field f.
  4. Instead, it has a property f returning an F-model instance, e.g.:
class M(Model):
     @property
     def my_prop(self):
        return self.foo.my_prop

Then, the serializer will fail to generate the URL, because the HyperlinkedRelatedField
cannot cope with the fact that django.db.models.base.Model#serializable_value
returns the PK, if the model returns a field with that name, or else the model instance.

If we disable the pk-only-optimization, HyperlinkedRelatedField will not call
django.db.models.base.Model#serializable_value, thus disabling the ode path affected
by this bug.

Another idea would be to force the use of the PK for these property based
HyperlinkedRelatedFields by specifying the sorce attribute of the HyperlinkedRelatedField.
But that doesn't work, cause some serializers must support properties and ForeinKeys at
the same time.

This a regression to DRF 2, as the minimal example works there flawlessly.

Let's run manage.py shell to find out more details

Imitialize stuff (you can ignore this):

In [3]: from lvm.models import LogicalVolume
 
 
In [7]: lv = LogicalVolume.objects.get()
 
 
In [10]: from volumes.restapi import BlockVolumeSerializer
 

# For Mocking Request objects. 
In [14]: class Yup(object):
   ....:     def __init__(self, **kwargs):
   ....:         self.__dict__.update(**kwargs)
   ....:     def build_absolute_uri(self, url):
   ....:         return 'http://localhost' + url
   ....:    

In [19]: lvs = BlockVolumeSerializer(lv, context={'request': Yup(path='/openattic/api/volumes', GET={})})

In [10]: lvs
Out[10]: 
BlockVolumeSerializer(<LogicalVolume: lv1>, context={'request': <__main__.Yup object>}):
    type = ContentTypeSerializer(read_only=True, source='volume_type'):
        url = HyperlinkedIdentityField(view_name='contenttype-detail')
        app_label = CharField(max_length=100, required=True)
        model = CharField(label='Python model class name', max_length=100, required=True)
        class Meta:
            validators = [<UniqueTogetherValidator(queryset=ContentType.objects.all(), fields=(u'app_label', u'model'))>]
    host = HyperlinkedRelatedField(read_only=True, view_name='host-detail')
    path = CharField()
    perfdata = SerializerMethodField('get_performance_data')

Here is the Trachback as seen above:

In [32]: lvs.data
---------------------------------------------------------------------------
ImproperlyConfigured                      Traceback (most recent call last)
<ipython-input-32-e553062931ab> in <module>()
----> 1 lvs.data
 
/usr/lib/python2.7/dist-packages/rest_framework/serializers.pyc in data(self)
    501     @property
    502     def data(self):
--> 503         ret = super(Serializer, self).data
    504         return ReturnDict(ret, serializer=self)
    505 
 
/usr/lib/python2.7/dist-packages/rest_framework/serializers.pyc in data(self)
    237         if not hasattr(self, '_data'):
    238             if self.instance is not None and not getattr(self, '_errors', None):
--> 239                 self._data = self.to_representation(self.instance)
    240             elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
    241                 self._data = self.to_representation(self.validated_data)
 
/usr/lib/python2.7/dist-packages/rest_framework/serializers.pyc in to_representation(self, instance)
    470                 ret[field.field_name] = None
    471             else:
--> 472                 ret[field.field_name] = field.to_representation(attribute)
    473 
    474         return ret
 
/usr/lib/python2.7/dist-packages/rest_framework/relations.pyc in to_representation(self, value)
    353                     "entries in your URL conf." % value_string
    354                 )
--> 355             raise ImproperlyConfigured(msg % self.view_name)
    356 
    357         if url is None:
 
ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "host-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.

But, this works, if I call to_representation on the field directly:

In [31]: lvs.fields['host'].to_representation(lv.host)
Out[31]: u'http://localhost/api/hosts/1'

And it works, if I append 'pk' to the .sources_attrs of the field:

In [133]: lvs.fields['host'].source_attrs = [u'host', 'pk']

and it works, if I call reverse directly:

In [24]: reverse('host-detail', kwargs={'pk': lv.host.pk}, request=Yup(path='/openattic/api/volumes', GET={}))
Out[24]: u'http://localhost/api/hosts/1'

I've debuged a little bit and found PKOnlyObject().pk to be the actual object (Host), and not the primary key (e.g. 5), thus I'm guessing the root cause there.

See also our Issue: https://tracker.openattic.org/browse/OP-1676

EDIT: let's deep into the PKOnlyObject().pk issue:

In [60]: type(lvs.fields['host'].get_attribute(lv).pk)
Out[60]: ifconfig.models.Host  # if this is int, it works as expected.

In [65]: lvs.fields['host'].source_attrs[:-1]
Out[65]: []

In [70]: lv.serializable_value('host')
Out[70]: <Host: oa-xen-1.oa.master.dns>

Turns out, serializable_value returns either int or Host, if the field can be found by name.

EDIT2, On my other system, to_representation actually worked with this broken PKOnlyObject().pk.

This is the output of the other system:

In [25]: reverse('host-detail', kwargs={'pk': lv.host.pk}, request=Yup(path='/openattic/api/volumes', GET={}))
Out[25]: u'http://localhost/api/hosts/1'

In [26]: reverse('host-detail', kwargs={'pk': lv.host}, request=Yup(path='/openattic/api/volumes', GET={}))
Out[26]: u'http://localhost/api/hosts/ubuntubox'

And http://localhost/api/hosts/ubuntubox is actually invalid.

EDIT3:
The error seems to be related to django.db.models.base.Model#serializable_value which returns either the related object or the primary key of the object dependent on self._meta.get_field(field_name) finding the field or not:

    def serializable_value(self, field_name):
        try:
            field = self._meta.get_field(field_name)
        except FieldDoesNotExist:
            return getattr(self, field_name)  # returns the Host object
        return getattr(self, field.attname) # returns the Host's primary key.

Edit4:

This is our workaround: https://bitbucket.org/openattic/openattic/pull-requests/499/op-1676-workaround-for/diff

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions