Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get_url(): AttributeError: 'OrderedDict' object has no attribute 'pk' #2638

Closed
ghost opened this issue Mar 5, 2015 · 11 comments
Closed

get_url(): AttributeError: 'OrderedDict' object has no attribute 'pk' #2638

ghost opened this issue Mar 5, 2015 · 11 comments

Comments

@ghost
Copy link

ghost commented Mar 5, 2015

Testing code:

class UserViewSet(viewsets.ModelViewSet):
    queryset = get_user_model().objects.all()
    serializer_class = UserSerializer
    permission_classes = (permissions.IsAdminUser,)

    def perform_create(self, serializer):
        email = serializer.data['email']
        password = serializer.data['password']
        extra_fields = {}
        if 'is_staff' in serializer.data:
            extra_fields['is_staff'] = serializer.data['is_staff']
        if 'is_active' in serializer.data:
            extra_fields['is_active'] = serializer.data['is_active']
        get_user_model().objects.create_user(email, password, **extra_fields)

Error:

[05/Mar/2015 03:09:44] ERROR [base:231] Internal Server Error: /api/users/
Traceback (most recent call last):
  File "/env/lib/python2.7/site-packages/django/core/handlers/base.py", line 111, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/env/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 57, in wrapped_view
    return view_func(*args, **kwargs)
  File "/env/src/djangorestframework/rest_framework/viewsets.py", line 85, in view
    return self.dispatch(request, *args, **kwargs)
  File "/env/src/djangorestframework/rest_framework/views.py", line 452, in dispatch
    response = self.handle_exception(exc)
  File "/env/src/djangorestframework/rest_framework/views.py", line 449, in dispatch
    response = handler(request, *args, **kwargs)
  File "/env/src/djangorestframework/rest_framework/mixins.py", line 20, in create
    self.perform_create(serializer)
  File "/api/views.py", line 29, in perform_create
    email = serializer.data['email']
  File "/env/src/djangorestframework/rest_framework/serializers.py", line 466, in data
    ret = super(Serializer, self).data
  File "/env/src/djangorestframework/rest_framework/serializers.py", line 215, in data
    self._data = self.to_representation(self.validated_data)
  File "/env/src/djangorestframework/rest_framework/serializers.py", line 435, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/env/src/djangorestframework/rest_framework/relations.py", line 267, in to_representation
    return self.get_url(value, self.view_name, request, format)
  File "/env/src/djangorestframework/rest_framework/relations.py", line 202, in get_url
    if obj.pk is None:
AttributeError: 'OrderedDict' object has no attribute 'pk'
[05/Mar/2015 03:09:44] "POST /api/users/ HTTP/1.1" 500 120552

Root cause: there's no pk in the OrderedDict yet at that time.

@ghost ghost mentioned this issue Mar 5, 2015
@jpadilla
Copy link
Member

jpadilla commented Mar 5, 2015

@zhigang you should really be calling serializer.save() or better yet a custom create() on your serializer. There are a couple of examples in the Serializers docs.

@ghost
Copy link
Author

ghost commented Mar 5, 2015

@jpadilla : thanks and I think you are right I should override the serializer create() method to do this.

But it seems the issue alone needs a code fix. Let's keep it open for now.

@tomchristie
Copy link
Member

Can we reduce this to a more minimal example?

@ghost
Copy link
Author

ghost commented Mar 7, 2015

Code to reproduce it:

views.py:

from django.contrib.auth import get_user_model

from rest_framework import permissions, serializers, viewsets


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ('url', 'id', 'username', 'email', 'password', 'is_active', 'is_staff', 'is_superuser', 'date_joined', 'last_login')


class UserViewSet(viewsets.ModelViewSet):
    queryset = get_user_model().objects.all()
    serializer_class = UserSerializer
    permission_classes = (permissions.IsAdminUser,)

    def perform_create(self, serializer):
        username = serializer.data['username']
        email = serializer.data['email']
        password = serializer.data['password']
        extra_fields = {}
        if 'is_staff' in serializer.data:
            extra_fields['is_staff'] = serializer.data['is_staff']
        if 'is_active' in serializer.data:
            extra_fields['is_active'] = serializer.data['is_active']
        get_user_model().objects.create_user(username, email, password, **extra_fields)

urls.py:

from django.conf.urls import patterns, include, url
from django.contrib import admin

from rest_framework import routers

from views import UserViewSet


router = routers.DefaultRouter()
router.register('users', UserViewSet)


urlpatterns = patterns('',
    url(r'^api/', include(router.urls)),
    url(r'^api/api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    url(r'^admin/', include(admin.site.urls)),
)

POST to http://127.0.0.1:8000/api/users/

Patch to fix this: #2641

@carltongibson
Copy link
Collaborator

OK. I've had a play with this in the shell. This comes up if you call data without save on a HyperlinedModelSerializer containing a url field.

  1. With the recommended behaviour, calling save or create on your serializer (as per @jpadilla's first comment here) you're never going to see this.
  2. If there's a legitimate reason not to do that you can access validated_data in your custom perform_create.

On this basis I'm going to close this as wrong usage.

(Mis-using data seems a common issue with the move to 3.0 — probably because all the older examples used it. A more prominent discussion of the various properties here may be worthwhile.)

@carltongibson
Copy link
Collaborator

Having looked back at #2637 I have a slight niggle about whether there's some example where you might legitimately want to call data and a HyperlinedModelSerializer containing a url field to serialise an unsaved object.

My first thought there is "Just remove the field" — i.e. it's an edge-case that the user can handle easily themselves — but if anybody wants to drop a failing test case in here we can look at re-opening again.

@ghost
Copy link
Author

ghost commented Mar 8, 2015

What if we just merge #2641 ? Is that better than just saying: don't do it?

@flamingofugang
Copy link

I faced the same issue when I try to upload an image file to the DRF REST interface.

There is no problem when I use 'curl -X POST'. However, when I use angularjs http post service (ajax call), I got the same error message, said function 'get_url()' raised 'obj.pk is none' error.

Here is the models.py

def upload_to(instance, filename):
    return 'post_image/{0}/{1}'.format(instance.author.id, filename)

class PostImage(models.Model):
    author = models.ForeignKey(Author, blank=False, editable=False, related_name='images')
    post = models.ForeignKey(Post, blank=False, editable=False, related_name='images')

    image = models.ImageField(_('image'), blank=True, null=True, upload_to=upload_to)
    created = models.DateTimeField(editable=False)
    updated = models.DateTimeField(editable=False)

    def __unicode__(self):
        return self.name

    def save(self, *args, **kwargs):
        ''' On save, update timestamps '''
        if not self.id:
            self.created = timezone.now()
        self.updated = timezone.now()
        return super(PostImage, self).save(*args, **kwargs)

Here is the serializers.py

class PostImageSerializer(serializers.HyperlinkedModelSerializer):
    author = serializers.HyperlinkedRelatedField(read_only=True, view_name='author-detail')
    post = serializers.HyperlinkedRelatedField(read_only=True, view_name='post-detail')

    class Meta:
        model = PostImage
        fields = ('url', 'image', 'author', 'post', 'created', 'updated')

Here is the views.py

class PostImageView(generics.ListCreateAPIView):
    """
    List and Create post image endpoint
    Allowed request method: Get, Post
    """
    serializer_class = PostImageSerializer
    permission_classes = [permissions.IsAuthenticated]
    parser_classes = (FormParser, MultiPartParser, FileUploadParser,)

    def get_queryset(self):
        queryset = super(PostImageView, self).get_queryset()
        return queryset.filter(post__pk=self.kwargs['pk'])

    def perform_create(self, serializer):
        post = Post.objects.get(pk=self.kwargs['pk'])
        if 'upload' in self.request.data:
            file_obj = self.request.data['upload']
            serializer.save(author=self.request.user, post=post, image=file_obj)
            return Response(status=status.HTTP_201_CREATED)
        else:
            return Response(status=status.HTTP_400_BAD_REQUEST)

@xordoquy
Copy link
Collaborator

Unless we have a failing test case that demonstrate the error is on DRF side, there isn't much we can do.
Also note that perform_create shouldn't return anything (see rest_framework/mixins.py)

@allardbrain
Copy link

Having looked back at #2637 I have a slight niggle about whether there's some example where you might legitimately want to call data and a HyperlinedModelSerializer containing a url field to serialise an unsaved object.

My first thought there is "Just remove the field" — i.e. it's an edge-case that the user can handle easily themselves — but if anybody wants to drop a failing test case in here we can look at re-opening again.

All these years later, this proved to me massively helpful! Thanks so much. You ended a 2-hour rabbit hole I just spent trying to resolve this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants
@carltongibson @jpadilla @tomchristie @xordoquy @flamingofugang @allardbrain and others