Skip to content

WIP: Error handling #787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 20 commits into from
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +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]
## [3.2.0] - pending

### Fixed
### Added

* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
* Added support for serializiing complex structures as attributes. For details please reffer to #769
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`

## [3.1.0] - 2020-02-08

Expand Down
238 changes: 238 additions & 0 deletions example/tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import pytest
from django.test import override_settings
from django.urls import reverse, path

from example.models import Blog
from rest_framework_json_api import serializers
from rest_framework import views


# serializers
class CommentAttachmentSerializer(serializers.Serializer):
data = serializers.CharField(allow_null=False, required=True)

def validate_data(self, value):
if value and len(value) < 10:
raise serializers.ValidationError('Too short data')


class CommentSerializer(serializers.Serializer):
attachments = CommentAttachmentSerializer(many=True, required=False)
attachment = CommentAttachmentSerializer(required=False)
one_more_attachment = CommentAttachmentSerializer(required=False)
body = serializers.CharField(allow_null=False, required=True)


class EntrySerializer(serializers.Serializer):
blog = serializers.IntegerField()
comments = CommentSerializer(many=True, required=False)
comment = CommentSerializer(required=False)
headline = serializers.CharField(allow_null=True, required=True)
body_text = serializers.CharField()

def validate(self, attrs):
body_text = attrs['body_text']
if len(body_text) < 5:
raise serializers.ValidationError({'body_text': 'Too short'})


# view
class DummyTestView(views.APIView):
serializer_class = EntrySerializer
resource_name = 'entries'

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)


urlpatterns = [
path(r'^entries-nested/$', DummyTestView.as_view(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regex not needed on path()

name='entries-nested-list')
]


@pytest.fixture(scope='function')
def some_blog(db):
return Blog.objects.create(name='Some Blog', tagline="It's a blog")


def perform_error_test(client, data, expected_pointer, errors_count=1):
with override_settings(
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True,
ROOT_URLCONF=__name__
):
url = reverse('entries-nested-list')
response = client.post(url, data=data)

errors = response.data

assert len(errors) == errors_count
assert errors[0]['source']['pointer'] == expected_pointer


def test_first_level_attribute_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
}
}
}
perform_error_test(client, data, '/data/attributes/headline')


def test_first_level_custom_attribute_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body',
'headline': 'headline'
}
}
}
with override_settings(JSON_API_FORMAT_FIELD_NAMES='underscore'):
perform_error_test(client, data, '/data/attributes/body_text')


def test_second_level_array_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
'headline': 'headline',
'comments': [
{
}
]
}
}
}

perform_error_test(client, data, '/data/attributes/comments/0/body')


def test_second_level_dict_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
'headline': 'headline',
'comment': {}
}
}
}

perform_error_test(client, data, '/data/attributes/comment/body')


def test_third_level_array_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
'headline': 'headline',
'comments': [
{
'body': 'test comment',
'attachments': [
{
}
]
}
]
}
}
}

perform_error_test(client, data, '/data/attributes/comments/0/attachments/0/data')


def test_third_level_custom_array_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
'headline': 'headline',
'comments': [
{
'body': 'test comment',
'attachments': [
{
'data': 'text'
}
]
}
]
}
}
}

perform_error_test(client, data, '/data/attributes/comments/0/attachments/0/data')


def test_third_level_dict_error(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
'headline': 'headline',
'comments': [
{
'body': 'test comment',
'attachment': {}
}
]
}
}
}

perform_error_test(client, data, '/data/attributes/comments/0/attachment/data')


def test_many_third_level_dict_errors(client, some_blog):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body_text': 'body_text',
'headline': 'headline',
'comments': [
{
'attachment': {}
}
]
}
}
}

perform_error_test(client, data, '/data/attributes/comments/0/body', 2)


@pytest.mark.filterwarning('default::DeprecationWarning:rest_framework_json_api.serializers')
def test_deprecation_warning(recwarn):
class DummyNestedSerializer(serializers.Serializer):
field = serializers.CharField()

class DummySerializer(serializers.Serializer):
nested = DummyNestedSerializer(many=True)

assert len(recwarn) == 1
warning = recwarn.pop(DeprecationWarning)
assert warning
assert str(warning.message).startswith('Rendering')
86 changes: 85 additions & 1 deletion example/tests/unit/test_renderers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json

import pytest
from django.test import override_settings
from django.utils import timezone

from rest_framework_json_api import serializers, views
from rest_framework_json_api.renderers import JSONRenderer

from example.models import Author, Comment, Entry
from example.models import Author, Comment, Entry, Blog


# serializers
Expand Down Expand Up @@ -38,6 +40,31 @@ class JSONAPIMeta:
included_resources = ('related_models',)


class EntryDRFSerializers(serializers.ModelSerializer):

class Meta:
model = Entry
fields = ('headline', 'body_text')
read_only_fields = ('tags',)


class CommentWithNestedFieldsSerializer(serializers.ModelSerializer):
entry = EntryDRFSerializers()

class Meta:
model = Comment
exclude = ('created_at', 'modified_at', 'author')
# fields = ('entry', 'body', 'author',)


class AuthorWithNestedFieldsSerializer(serializers.ModelSerializer):
comments = CommentWithNestedFieldsSerializer(many=True)

class Meta:
model = Author
fields = ('name', 'email', 'comments')


# views
class DummyTestViewSet(views.ModelViewSet):
queryset = Entry.objects.all()
Expand All @@ -49,6 +76,12 @@ class ReadOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
serializer_class = DummyTestSerializer


class AuthorWithNestedFieldsViewSet(views.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorWithNestedFieldsSerializer
resource_name = 'authors'


def render_dummy_test_serialized_view(view_class, instance):
serializer = view_class.serializer_class(instance=instance)
renderer = JSONRenderer()
Expand Down Expand Up @@ -138,3 +171,54 @@ def test_extract_relation_instance(comment):
field=serializer.fields['blog'], resource_instance=comment
)
assert got == comment.entry.blog


def test_attribute_rendering_strategy(db):
# setting up
blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
entry = Entry.objects.create(
blog=blog,
headline='headline',
body_text='body_text',
pub_date=timezone.now(),
mod_date=timezone.now(),
n_comments=0,
n_pingbacks=0,
rating=3
)

author = Author.objects.create(name='some_author', email='[email protected]')
entry.authors.add(author)

Comment.objects.create(
entry=entry,
body='testing one two three',
author=Author.objects.first()
)

with override_settings(
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True):
rendered = render_dummy_test_serialized_view(AuthorWithNestedFieldsViewSet, author)
result = json.loads(rendered.decode())

expected = {
"data": {
"type": "authors",
"id": "1",
"attributes": {
"name": "some_author",
"email": "[email protected]",
"comments": [
{
"id": 1,
"entry": {
'headline': 'headline',
'body_text': 'body_text',
},
"body": "testing one two three"
}
]
}
}
}
assert expected == result
3 changes: 1 addition & 2 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
EntryDRFSerializers,
EntrySerializer,
ProjectSerializer,
ProjectTypeSerializer
)
ProjectTypeSerializer)

HTTP_422_UNPROCESSABLE_ENTITY = 422

Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ DJANGO_SETTINGS_MODULE=example.settings.test
filterwarnings =
error::DeprecationWarning
error::PendingDeprecationWarning
ignore::DeprecationWarning:rest_framework_json_api.serializers
Loading