Skip to content
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

Browsable api raises AttributeError when posting raw data not containing data for nested serializer #3260

Closed
jgadelange opened this issue Aug 11, 2015 · 10 comments
Labels
Milestone

Comments

@jgadelange
Copy link
Contributor

When I post an empty object ({}) using the raw data form in the browsable API I get an AttributeError, when I have a nested serializer defined on the serializer set for that view. Example code and stack trace can be found below.

Example code:

class NestedSerializer(serializers.Serializer):
    sub_field = serializers.CharField()


class MainSerializer(serializers.Serializer):
    nested = NestedSerializer()


class TestView(APIView):
    serializer_class = MainSerializer

    def get_serializer(self, **kwargs):
        return self.serializer_class(**kwargs)

    def post(self, request, format=None):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            serializer.create(serializer.validated_data)
            return Response(serializer.data, status=status.HTTP_200_OK)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Traceback (most recent call last):
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/core/handlers/base.py", line 164, in get_response
    response = response.render()
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/response.py", line 158, in render
    self.content = self.rendered_content
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/response.py", line 60, in rendered_content
    ret = renderer.render(self.data, media_type, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 669, in render
    context = self.get_context(data, accepted_media_type, renderer_context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 646, in get_context
    'post_form': self.get_rendered_html_form(data, view, 'POST', request),
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 513, in get_rendered_html_form
    [('template', 'rest_framework/api_form.html')]
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 367, in render
    return template.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/backends/django.py", line 74, in render
    return self.template.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 209, in render
    return self._render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 201, in _render
    return self.nodelist.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 903, in render
    bit = self.render_node(node, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/debug.py", line 79, in render_node
    return node.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/defaulttags.py", line 217, in render
    nodelist.append(node.render(context))
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/defaulttags.py", line 329, in render
    return nodelist.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 903, in render
    bit = self.render_node(node, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/debug.py", line 79, in render_node
    return node.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 1195, in render
    return func(*resolved_args, **resolved_kwargs)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/templatetags/rest_framework.py", line 31, in render_field
    return renderer.render_field(field, style)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 325, in render_field
    field = field.as_form_field()
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/utils/serializer_helpers.py", line 99, in as_form_field
    for key, value in self.value.items():
AttributeError: 'NoneType' object has no attribute 'items'
@tomchristie tomchristie added this to the 3.2.2 Release milestone Aug 11, 2015
@tomchristie
Copy link
Member

Okay, believe this is now resolved in master, to be released as 3.2.2 either today or later this week.

Desperately need some good tests around all this but this quick fix will need to do for now.

@jgadelange
Copy link
Contributor Author

I was just working on a pull request for this myself. I was planning to add the following code to NestedBoundField:

def __init__(self, field, value, errors, prefix=''):
        if value is None:
            value = {}
        super(NestedBoundField, self).__init__(field, value, errors, prefix)

I believe the current fix is not correct, since it will also "remove" the form for the nested serializer all together:
django_rest_framework

@tomchristie
Copy link
Member

@jgadelange Gotcha - best thing would probably be to go ahead with that pull request then.

@jgadelange
Copy link
Contributor Author

Hmmmm... when testing this some more I found an other error. When leaving out a required field the errors on the 'NestedBoundedField' is a list ([u'This field is required.']). I will look into this more tomorrow.

Traceback (most recent call last):
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/core/handlers/base.py", line 164, in get_response
    response = response.render()
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/response.py", line 158, in render
    self.content = self.rendered_content
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/response.py", line 71, in rendered_content
    ret = renderer.render(self.data, media_type, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 669, in render
    context = self.get_context(data, accepted_media_type, renderer_context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 646, in get_context
    'post_form': self.get_rendered_html_form(data, view, 'POST', request),
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 513, in get_rendered_html_form
    [('template', 'rest_framework/api_form.html')]
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 367, in render
    return template.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/backends/django.py", line 74, in render
    return self.template.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 209, in render
    return self._render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 201, in _render
    return self.nodelist.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 903, in render
    bit = self.render_node(node, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/debug.py", line 79, in render_node
    return node.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/defaulttags.py", line 217, in render
    nodelist.append(node.render(context))
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/defaulttags.py", line 329, in render
    return nodelist.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 903, in render
    bit = self.render_node(node, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/debug.py", line 79, in render_node
    return node.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 1195, in render
    return func(*resolved_args, **resolved_kwargs)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/templatetags/rest_framework.py", line 31, in render_field
    return renderer.render_field(field, style)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/renderers.py", line 337, in render_field
    return template.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/backends/django.py", line 74, in render
    return self.template.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 209, in render
    return self._render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 201, in _render
    return self.nodelist.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/base.py", line 903, in render
    bit = self.render_node(node, context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/debug.py", line 79, in render_node
    return node.render(context)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/django/template/defaulttags.py", line 161, in render
    values = list(values)
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/utils/serializer_helpers.py", line 98, in __iter__
    yield self[field.field_name]
  File "<path_to_virtualenv>/lib/python2.7/site-packages/rest_framework/utils/serializer_helpers.py", line 103, in __getitem__
    error = self.errors.get(key) if self.errors else None
AttributeError: 'list' object has no attribute 'get'

@cancan101
Copy link
Contributor

cancan101 commented Jan 5, 2017

I seeing this in v3.5.3:

AttributeError: 'list' object has no attribute 'get'
  File "django/core/handlers/exception.py", line 39, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "django/core/handlers/base.py", line 217, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 215, in _get_response
    response = response.render()
  File "django/template/response.py", line 109, in render
    self.content = self.rendered_content
  File "rest_framework/response.py", line 72, in rendered_content
    ret = renderer.render(self.data, accepted_media_type, context)
  File "rest_framework/renderers.py", line 701, in render
    context = self.get_context(data, accepted_media_type, renderer_context)
  File "rest_framework/renderers.py", line 673, in get_context
    'put_form': self.get_rendered_html_form(data, view, 'PUT', request),
  File "rest_framework/renderers.py", line 489, in get_rendered_html_form
    return self.render_form_for_serializer(existing_serializer)
  File "rest_framework/renderers.py", line 517, in render_form_for_serializer
    {'style': {'template_pack': 'rest_framework/horizontal'}}
  File "rest_framework/renderers.py", line 369, in render
    return template_render(template, context)
  File "rest_framework/compat.py", line 306, in template_render
    return template.render(context, request=request)
  File "django/template/backends/django.py", line 66, in render
    return self.template.render(context)
  File "django/template/base.py", line 208, in render
    return self._render(context)
  File "newrelic/api/function_trace.py", line 98, in dynamic_wrapper
    return wrapped(*args, **kwargs)
  File "django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "django/template/base.py", line 994, in render
    bit = node.render_annotated(context)
  File "django/template/base.py", line 961, in render_annotated
    return self.render(context)
  File "django/template/defaulttags.py", line 209, in render
    nodelist.append(node.render_annotated(context))
  File "django/template/base.py", line 961, in render_annotated
    return self.render(context)
  File "django/template/defaulttags.py", line 315, in render
    return nodelist.render(context)
  File "django/template/base.py", line 994, in render
    bit = node.render_annotated(context)
  File "django/template/base.py", line 961, in render_annotated
    return self.render(context)
  File "django/template/library.py", line 203, in render
    output = self.func(*resolved_args, **resolved_kwargs)
  File "rest_framework/templatetags/rest_framework.py", line 37, in render_field
    return renderer.render_field(field, style)
  File "rest_framework/renderers.py", line 348, in render_field
    return template_render(template, context)
  File "rest_framework/compat.py", line 306, in template_render
    return template.render(context, request=request)
  File "django/template/backends/django.py", line 66, in render
    return self.template.render(context)
  File "django/template/base.py", line 208, in render
    return self._render(context)
  File "newrelic/api/function_trace.py", line 98, in dynamic_wrapper
    return wrapped(*args, **kwargs)
  File "django/template/base.py", line 199, in _render
    return self.nodelist.render(context)
  File "django/template/base.py", line 994, in render
    bit = node.render_annotated(context)
  File "django/template/base.py", line 961, in render_annotated
    return self.render(context)
  File "django/template/defaulttags.py", line 165, in render
    values = list(values)
  File "rest_framework/utils/serializer_helpers.py", line 99, in __iter__
    yield self[field.field_name]
  File "rest_framework/utils/serializer_helpers.py", line 104, in __getitem__
    error = self.errors.get(key) if self.errors else None

And this is self:
<NestedBoundField value={} errors=[u'This field is required.']>

@cancan101
Copy link
Contributor

@tomchristie I think this issue still exists as the description above also matches my scenario.

@tomchristie
Copy link
Member

Able to replicate it in a super slimmed-down example case?

@cancan101
Copy link
Contributor

cancan101 commented Jan 7, 2017

@tomchristie https://www.dropbox.com/s/vm6denvw9depy5s/mysite.zip?dl=0
Using browsable API to POST to /person/ a payload of {}:
image

@aruseni
Copy link

aruseni commented Mar 3, 2017

I can still reproduce it in 3.5.4.

Models:

class NoteDescription(models.Model):
    color = models.CharField(max_length=100)


class Note(models.Model):
    content = models.CharField(max_length=100)
    description = models.ForeignKey(NoteDescription, on_delete=models.CASCADE)

Serializers:

class NoteDescriptionSerializer(serializers.ModelSerializer):
    class Meta:
        model = NoteDescription
        fields = ("color",)


class NoteSerializer(serializers.ModelSerializer):
    description = NoteDescriptionSerializer()

    class Meta:
        model = Note
        fields = ("content", "description")

A simple viewset to try it:

class NoteViewSet(viewsets.GenericViewSet):
    serializer_class = NoteSerializer

    def create(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        return Response(serializer.data)

Then you just need to make a POST request only containing {} with Browsable.

@xordoquy
Copy link
Collaborator

xordoquy commented Mar 4, 2017

Tested on one of my project with latest released DRF and got a correct response:

HTTP 400 Bad Request
Allow: GET, POST, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "lanes": [
        "This field is required."
    ],
    "name": [
        "This field is required."
    ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants