diff --git a/CHANGELOG.md b/CHANGELOG.md index f2dc90b7..85942770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/tests/test_parsers.py b/example/tests/test_parsers.py index 6ff2cfa7..3a2459c0 100644 --- a/example/tests/test_parsers.py +++ b/example/tests/test_parsers.py @@ -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): @@ -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' diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 7a940b6c..88c4f522 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -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] ) )