Skip to content

Commit ff0f93a

Browse files
authored
Avoid AttributeError for PUT and PATCH methods when using APIView (#778)
Fixes silenced AttributeError in the case when lookup_url_kwarg doesn't exist in a view
1 parent 5f19ef0 commit ff0f93a

File tree

3 files changed

+92
-3
lines changed

3 files changed

+92
-3
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
99
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
1010

11+
## [Unreleased]
12+
13+
### Fixed
14+
15+
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
16+
1117
## [3.1.0] - 2020-02-08
1218

1319
### Added

example/tests/test_parsers.py

+82
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import json
22
from io import BytesIO
33

4+
from django.conf.urls import url
45
from django.test import TestCase, override_settings
6+
from django.urls import reverse
7+
from rest_framework import views, status
58
from rest_framework.exceptions import ParseError
9+
from rest_framework.response import Response
10+
from rest_framework.test import APITestCase
611

12+
from rest_framework_json_api import serializers
713
from rest_framework_json_api.parsers import JSONParser
14+
from rest_framework_json_api.renderers import JSONRenderer
815

916

1017
class TestJSONParser(TestCase):
@@ -69,3 +76,78 @@ def test_parse_invalid_data_key(self):
6976

7077
with self.assertRaises(ParseError):
7178
parser.parse(stream, None, self.parser_context)
79+
80+
81+
class DummyDTO:
82+
def __init__(self, response_dict):
83+
for k, v in response_dict.items():
84+
setattr(self, k, v)
85+
86+
@property
87+
def pk(self):
88+
return self.id if hasattr(self, 'id') else None
89+
90+
91+
class DummySerializer(serializers.Serializer):
92+
body = serializers.CharField()
93+
id = serializers.IntegerField()
94+
95+
96+
class DummyAPIView(views.APIView):
97+
parser_classes = [JSONParser]
98+
renderer_classes = [JSONRenderer]
99+
resource_name = 'dummy'
100+
101+
def patch(self, request, *args, **kwargs):
102+
serializer = DummySerializer(DummyDTO(request.data))
103+
return Response(status=status.HTTP_200_OK, data=serializer.data)
104+
105+
106+
urlpatterns = [
107+
url(r'repeater$', DummyAPIView.as_view(), name='repeater'),
108+
]
109+
110+
111+
class TestParserOnAPIView(APITestCase):
112+
113+
def setUp(self):
114+
class MockRequest(object):
115+
def __init__(self):
116+
self.method = 'PATCH'
117+
118+
request = MockRequest()
119+
# To be honest view string isn't resolved into actual view
120+
self.parser_context = {'request': request, 'kwargs': {}, 'view': 'DummyAPIView'}
121+
122+
self.data = {
123+
'data': {
124+
'id': 123,
125+
'type': 'strs',
126+
'attributes': {
127+
'body': 'hello'
128+
},
129+
}
130+
}
131+
132+
self.string = json.dumps(self.data)
133+
134+
def test_patch_doesnt_raise_attribute_error(self):
135+
parser = JSONParser()
136+
137+
stream = BytesIO(self.string.encode('utf-8'))
138+
139+
data = parser.parse(stream, None, self.parser_context)
140+
141+
assert data['id'] == 123
142+
assert data['body'] == 'hello'
143+
144+
@override_settings(ROOT_URLCONF=__name__)
145+
def test_patch_request(self):
146+
url = reverse('repeater')
147+
data = self.data
148+
data['data']['type'] = 'dummy'
149+
response = self.client.patch(url, data=data)
150+
data = response.json()
151+
152+
assert data['data']['id'] == str(123)
153+
assert data['data']['attributes']['body'] == 'hello'

rest_framework_json_api/parsers.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,14 @@ def parse(self, stream, media_type=None, parser_context=None):
144144
raise ParseError("The resource identifier object must contain an 'id' member")
145145

146146
if request.method in ('PATCH', 'PUT'):
147-
lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field
148-
if str(data.get('id')) != str(view.kwargs[lookup_url_kwarg]):
147+
lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or \
148+
getattr(view, 'lookup_field', None)
149+
if lookup_url_kwarg and str(data.get('id')) != str(view.kwargs[lookup_url_kwarg]):
149150
raise exceptions.Conflict(
150151
"The resource object's id ({data_id}) does not match url's "
151152
"lookup id ({url_id})".format(
152153
data_id=data.get('id'),
153-
url_id=view.kwargs[view.lookup_field]
154+
url_id=view.kwargs[lookup_url_kwarg]
154155
)
155156
)
156157

0 commit comments

Comments
 (0)