Add _reason_phrase to Response

Improve reason_phrase for error details that are length 1 lists.

Use reason_phrase property rather than non-public attribute

Replace f-strings with format for py35 compatability

Replace missed f-string with format
This commit is contained in:
Timothy McCurrach 2020-11-19 18:29:41 +00:00
parent 96993d817a
commit 3e1fca171b
4 changed files with 72 additions and 12 deletions

View File

@ -17,9 +17,16 @@ class Response(SimpleTemplateResponse):
arbitrary media types. arbitrary media types.
""" """
def __init__(self, data=None, status=None, def __init__(
template_name=None, headers=None, self,
exception=False, content_type=None): data=None,
status=None,
template_name=None,
headers=None,
exception=False,
content_type=None,
reason=None,
):
""" """
Alters the init arguments slightly. Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'. For example, drop 'template_name', and instead use 'data'.
@ -41,6 +48,8 @@ class Response(SimpleTemplateResponse):
self.template_name = template_name self.template_name = template_name
self.exception = exception self.exception = exception
self.content_type = content_type self.content_type = content_type
if reason:
self.reason_phrase = "{} ({})".format(self.status_text, reason)
if headers: if headers:
for name, value in headers.items(): for name, value in headers.items():

View File

@ -8,6 +8,7 @@ from django.http import Http404
from django.http.response import HttpResponseBase from django.http.response import HttpResponseBase
from django.utils.cache import cc_delim_re, patch_vary_headers from django.utils.cache import cc_delim_re, patch_vary_headers
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View from django.views.generic import View
@ -92,11 +93,15 @@ def exception_handler(exc, context):
if isinstance(exc.detail, (list, dict)): if isinstance(exc.detail, (list, dict)):
data = exc.detail data = exc.detail
reason = _('See response body for full details')
else: else:
data = {'detail': exc.detail} data = {"detail": exc.detail}
reason = str(exc.detail)
if isinstance(exc.detail, list) and len(exc.detail) == 1:
reason = exc.detail[0]
set_rollback() set_rollback()
return Response(data, status=exc.status_code, headers=headers) return Response(data, status=exc.status_code, headers=headers, reason=reason)
return None return None

View File

@ -32,6 +32,7 @@ class MockTextMediaRenderer(BaseRenderer):
DUMMYSTATUS = status.HTTP_200_OK DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent' DUMMYCONTENT = 'dummycontent'
DUMMYREASON = 'dummyreason'
def RENDERER_A_SERIALIZER(x): def RENDERER_A_SERIALIZER(x):
@ -78,6 +79,12 @@ class MockViewSettingContentType(APIView):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview') return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
class MockViewSettingReason(APIView):
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, reason=DUMMYREASON)
class JSONView(APIView): class JSONView(APIView):
parser_classes = (JSONParser,) parser_classes = (JSONParser,)
@ -125,7 +132,8 @@ urlpatterns = [
path('html1', HTMLView1.as_view()), path('html1', HTMLView1.as_view()),
path('html_new_model', HTMLNewModelView.as_view()), path('html_new_model', HTMLNewModelView.as_view()),
path('html_new_model_viewset', include(new_model_viewset_router.urls)), path('html_new_model_viewset', include(new_model_viewset_router.urls)),
path('restframework', include('rest_framework.urls', namespace='rest_framework')) path('restframework', include('rest_framework.urls', namespace='rest_framework')),
path('with_reason', MockViewSettingReason.as_view())
] ]
@ -283,3 +291,14 @@ class Issue807Tests(TestCase):
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
# self.assertContains(resp, 'Text comes here') # self.assertContains(resp, 'Text comes here')
# self.assertContains(resp, 'Text description.') # self.assertContains(resp, 'Text description.')
@override_settings(ROOT_URLCONF='tests.test_response')
class ReasonPhraseTests(TestCase):
def test_reason_is_set(self):
resp = self.client.get('/with_reason')
self.assertEqual("OK ({})".format(DUMMYREASON), resp.reason_phrase)
def test_reason_phrase_with_no_reason(self):
resp = self.client.get('/')
self.assertEqual("OK", resp.reason_phrase)

View File

@ -1,9 +1,11 @@
import copy import copy
import re
from django.test import TestCase from django.test import TestCase
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import APISettings, api_settings from rest_framework.settings import APISettings, api_settings
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
@ -12,6 +14,9 @@ from rest_framework.views import APIView
factory = APIRequestFactory() factory = APIRequestFactory()
JSON_ERROR = 'JSON parse error - Expecting value:' JSON_ERROR = 'JSON parse error - Expecting value:'
STATUS_CODE_400 = 'Bad Request'
REASON_PHRASE_RE = r"{} \({}.*\)".format(STATUS_CODE_400, JSON_ERROR)
VALIDATION_ERROR = "Data isn't valid!"
class BasicView(APIView): class BasicView(APIView):
@ -39,6 +44,11 @@ class ErrorView(APIView):
raise Exception raise Exception
class ValidationErrorView(APIView):
def get(self, request, *args, **kwargs):
raise ValidationError(VALIDATION_ERROR)
def custom_handler(exc, context): def custom_handler(exc, context):
if isinstance(exc, SyntaxError): if isinstance(exc, SyntaxError):
return Response({'error': 'SyntaxError'}, status=400) return Response({'error': 'SyntaxError'}, status=400)
@ -57,6 +67,11 @@ def error_view(request):
raise Exception raise Exception
@api_view(['GET'])
def validation_error_view(request):
raise ValidationError(VALIDATION_ERROR)
def sanitise_json_error(error_dict): def sanitise_json_error(error_dict):
""" """
Exact contents of JSON error messages depend on the installed version Exact contents of JSON error messages depend on the installed version
@ -69,32 +84,44 @@ def sanitise_json_error(error_dict):
class ClassBasedViewIntegrationTests(TestCase): class ClassBasedViewIntegrationTests(TestCase):
def setUp(self):
self.view = BasicView.as_view()
def test_400_parse_error(self): def test_400_parse_error(self):
request = factory.post('/', 'f00bar', content_type='application/json') request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request) response = BasicView.as_view()(request)
expected = { expected = {
'detail': JSON_ERROR 'detail': JSON_ERROR
} }
assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.status_code == status.HTTP_400_BAD_REQUEST
assert re.match(REASON_PHRASE_RE, response.reason_phrase)
assert sanitise_json_error(response.data) == expected assert sanitise_json_error(response.data) == expected
def test_400_validation_error(self):
request = factory.get('/')
response = ValidationErrorView.as_view()(request)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.reason_phrase == "{} ({})".format(STATUS_CODE_400, VALIDATION_ERROR)
assert response.data == [VALIDATION_ERROR]
class FunctionBasedViewIntegrationTests(TestCase): class FunctionBasedViewIntegrationTests(TestCase):
def setUp(self):
self.view = basic_view
def test_400_parse_error(self): def test_400_parse_error(self):
request = factory.post('/', 'f00bar', content_type='application/json') request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request) response = basic_view(request)
expected = { expected = {
'detail': JSON_ERROR 'detail': JSON_ERROR
} }
assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.status_code == status.HTTP_400_BAD_REQUEST
assert re.match(REASON_PHRASE_RE, response.reason_phrase)
assert sanitise_json_error(response.data) == expected assert sanitise_json_error(response.data) == expected
def test_400_validation_error(self):
request = factory.get('/')
response = validation_error_view(request)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.reason_phrase == "{} ({})".format(STATUS_CODE_400, VALIDATION_ERROR)
assert response.data == [VALIDATION_ERROR]
class TestCustomExceptionHandler(TestCase): class TestCustomExceptionHandler(TestCase):
def setUp(self): def setUp(self):