0
votes

I have to create an API for an existing Django based web app and I'm using DRF to do so. Although I've never used DRF before, everything was incredibly clear and fine until I ran into this problem. Here's the URL in question:

http://www.<customer-website-name>.com/api/friendship-requests/<sender-pk>/

According to the requirements spec that I was given, it's supposed to be handled like this:

  • [GET] Return a FriendshipRequest object that was sent to the currently authenticated user by a user whose pk = <sender-pk> or 404 if such request doesn't exist.
  • [PUT] Accepts an AcceptReject object (e.g., JSON {"accepted": false}). If accepted == True, clean up FriendshipRequest, create a new Friendship object, return 201; if accepted == False, mark FriendshipRequest as rejected, return 204

Here's the AcceptReject class and it's serializer:

class AcceptReject(object):
    def __init__(self, accepted):
        self.accepted = accepted

class AcceptRejectSerializer(serializers.Serializer):
    accepted = serializers.BooleanField()

This is the view I'm using to handle the url:

class FriendshipRequestDetail(mixins.RetrieveModelMixin,
            generics.GenericAPIView):

    def get_serializer_class(self):
        if self.request.method == 'PUT':
            return serializers.AcceptRejectSerializer
        return serializers.FriendshipRequestSerializer

    def get_object(self):
        sender = generics.get_object_or_404(get_user_model(),
                pk=self.kwargs['pk'])
        obj = generics.get_object_or_404(FriendshipRequest, 
                from_user=sender,
                to_user=self.request.user)

        self.check_object_permissions(self.request, obj)

        return obj

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return Response(status=status.HTTP_204_NO_CONTENT)

I naively thought that overriding get_serializer_class() to return a serializer based on request method would be enough, but it wasn't.

Also, as you can see, I haven't even started writing the PUT handler. Just it's presence is enough to cause the following error when I try to use the browsable api:

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/api/friendship-requests/5/

Django Version: 1.8.4
Python Version: 3.4.3
Installed Applications:
('django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.gis',
 'rest_framework',
 'rest_framework.authtoken',
 'friendship',
 'REDACTED.REDACTED')
Installed Middleware:
('django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django.middleware.security.SecurityMiddleware')


Traceback:
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  164.                 response = response.render()
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/django/template/response.py" in render
  158.             self.content = self.rendered_content
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/response.py" in rendered_content
  71.         ret = renderer.render(self.data, media_type, context)
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/renderers.py" in render
  669.         context = self.get_context(data, accepted_media_type, renderer_context)
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/renderers.py" in get_context
  614.         raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request)
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/renderers.py" in get_raw_data_form
  561.                 content = renderer.render(serializer.data, accepted, context)
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/serializers.py" in data
  487.         ret = super(Serializer, self).data
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/serializers.py" in data
  223.                 self._data = self.to_representation(self.instance)
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/serializers.py" in to_representation
  447.                 attribute = field.get_attribute(instance)
File "/home/manvis/REDACTED/env/lib/python3.4/site-packages/rest_framework/fields.py" in get_attribute
  418.             raise type(exc)(msg)

Exception Type: AttributeError at /api/friendship-requests/5/
Exception Value: Got AttributeError when attempting to get a value for field `accepted` on serializer `AcceptRejectSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `FriendshipRequest` instance.
Original exception text was: 'FriendshipRequest' object has no attribute 'accepted'.

Like I've said, it's my first time using DRF, but, if I understood the trace correctly, it seems that the error stems from DRF trying to render the form for the browsable api, unfortunately, I can't go without it, because a browsable api is also a requirement.

What else do I need to change/override in order to get browsable api working for this view?

1

1 Answers

0
votes

The problems comes from the fact, that you break REST convention. Instead of resource representation you use action in PUT request. In the REST you would rather have is_accepted or state field inside your FriendRequest object and by modification of this field you would accept or reject requests. I highly recommend you to point on this to designers of your spec.

In the browsable API DRF generates a form to update record, so it gets serializer by calling get_serializer() (which returns AcceptRejectSerializer) and transforms this serializer into form. Then it gets record by calling get_object() (which returns FriendshipRequest object) and tries to fill form with data got from the record. Obviously fields in record and serializer doesn't match, so you get an error.

As a shitty solution you can add accept attribute inside get_object() method:

def get_object(self):
    sender = generics.get_object_or_404(get_user_model(),
            pk=self.kwargs['pk'])
    obj = generics.get_object_or_404(FriendshipRequest, 
            from_user=sender,
            to_user=self.request.user)

    self.check_object_permissions(self.request, obj)

    obj.accept = False

    return obj

Or add it as into FriendshipRequest model:

class FriendshipRequest(...):
    # ...

    accept = False

Or you can try to override view to skip form filling step.