Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.

## [Unreleased]

### Fixed

* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`

## [3.1.0] - 2020-02-08

### Added
Expand Down
82 changes: 82 additions & 0 deletions example/tests/test_parsers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import json
from io import BytesIO

from django.conf.urls import url
from django.test import TestCase, override_settings
from django.urls import reverse
from rest_framework import views, status
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from rest_framework.test import APITestCase

from rest_framework_json_api import serializers
from rest_framework_json_api.parsers import JSONParser
from rest_framework_json_api.renderers import JSONRenderer


class TestJSONParser(TestCase):
Expand Down Expand Up @@ -69,3 +76,78 @@ def test_parse_invalid_data_key(self):

with self.assertRaises(ParseError):
parser.parse(stream, None, self.parser_context)


class DummyDTO:
def __init__(self, response_dict):
for k, v in response_dict.items():
setattr(self, k, v)

@property
def pk(self):
return self.id if hasattr(self, 'id') else None


class DummySerializer(serializers.Serializer):
body = serializers.CharField()
id = serializers.IntegerField()


class DummyAPIView(views.APIView):
parser_classes = [JSONParser]
renderer_classes = [JSONRenderer]
resource_name = 'dummy'

def patch(self, request, *args, **kwargs):
serializer = DummySerializer(DummyDTO(request.data))
return Response(status=status.HTTP_200_OK, data=serializer.data)


urlpatterns = [
url(r'repeater$', DummyAPIView.as_view(), name='repeater'),
]


class TestParserOnAPIView(APITestCase):

def setUp(self):
class MockRequest(object):
def __init__(self):
self.method = 'PATCH'

request = MockRequest()
# To be honest view string isn't resolved into actual view
self.parser_context = {'request': request, 'kwargs': {}, 'view': 'DummyAPIView'}

self.data = {
'data': {
'id': 123,
'type': 'strs',
'attributes': {
'body': 'hello'
},
}
}

self.string = json.dumps(self.data)

def test_patch_doesnt_raise_attribute_error(self):
parser = JSONParser()

stream = BytesIO(self.string.encode('utf-8'))

data = parser.parse(stream, None, self.parser_context)

assert data['id'] == 123
assert data['body'] == 'hello'

@override_settings(ROOT_URLCONF=__name__)
def test_patch_request(self):
url = reverse('repeater')
data = self.data
data['data']['type'] = 'dummy'
response = self.client.patch(url, data=data)
data = response.json()

assert data['data']['id'] == str(123)
assert data['data']['attributes']['body'] == 'hello'
7 changes: 4 additions & 3 deletions rest_framework_json_api/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,14 @@ def parse(self, stream, media_type=None, parser_context=None):
raise ParseError("The resource identifier object must contain an 'id' member")

if request.method in ('PATCH', 'PUT'):
lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field
if str(data.get('id')) != str(view.kwargs[lookup_url_kwarg]):
lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or \
getattr(view, 'lookup_field', None)
if lookup_url_kwarg and str(data.get('id')) != str(view.kwargs[lookup_url_kwarg]):
raise exceptions.Conflict(
"The resource object's id ({data_id}) does not match url's "
"lookup id ({url_id})".format(
data_id=data.get('id'),
url_id=view.kwargs[view.lookup_field]
url_id=view.kwargs[lookup_url_kwarg]
)
)

Expand Down