Search code examples
pythondjangotastypie

Multipart/Form-Data POST, PUT, PATCH in Django-Tastypie


I need to Patch user-profile picture using tastypie in django. I use Django 1.6, AngularJs and it uses normal Authentication() and DjangoAuthorization() classes of Django.

When i try to upload an image with 'CONTENT-TYPE:Multipart/form-data'. I get the following error.

error_message: "You cannot access body after reading from request's data stream"

Firstly I know that Tastypie has no-official support for Multi-part Form-Data. I'm hoping for a monkey patch or whatever that is working until tastypie gives support for Multipart-Form Data.

I have done some research on the above and i have looked into some of the questions posted:

References:

  1. Django-Tastypie Deserializing Multipart Form Data
  2. Accessing Post Data inside Tastypie

Though the above method seemed to be like a hack.. I wanted to give it a try in my Resources.

My Resource asfollows :


class UserProfileResource(ModelResource):
    user = fields.ToOneField(UserResource, 'user', full=True)
    profile_img = fields.FileField(attribute="img", null=True, blank=True)

    """docstring for ProfileResource"""
    def alter_list_data_to_serialize(self, request, data):
        if request.GET.get('meta_only'):
            return {'meta': data['meta']}
        return data

    def authorized_read_list(self, object_list, bundle):
        return object_list.filter(user=bundle.request.user).select_related()

    def obj_create(self, bundle, **kwargs):
        return super(UserProfileResource, self).obj_create(bundle, user=bundle.request.user)

    def dehydrate(self, bundle):
        bundle.data['groups'] = [g.name for g in bundle.request.user.groups.all()]
        return bundle

    """Deserialize for multipart Data"""
    def deserialize(self, request, data, format=None):
        if format is None:
            format = request.META.get('CONTENT_TYPE','application/json')
        if format == 'application/x-www-form-urlencoded':
            return request.POST
        elif format.startswith('multipart'):
            data = request.POST.copy()
            data.update(request.FILES)
            return data
        return super(UserProfileResource, self).deserialize(request, data, format)

    """PATCH For Making the request._body = FALSE"""
    def put_detail(self, request, **kwargs):
        if request.META.get('CONTENT_TYPE').startswith('multipart') and \
                not hasattr(request, '_body'):
            request._body = ''

        return super(UserProfileResource, self).put_detail(request, **kwargs)

    """PATCH for MAKING the request._body = FALSE"""
    def convert_post_to_VERB(request, verb):
        """
        Force Django to process the VERB. Monkey Patch for Multipart Data
        """
        if request.method == verb:
            if hasattr(request, '_post'):
                del (request._post)
                del (request._files)

            request._body  # now request._body is set
            request._read_started = False  # so it won't cause side effects
            try:
                request.method = "POST"
                request._load_post_and_files()
                request.method = verb
            except AttributeError:
                request.META['REQUEST_METHOD'] = 'POST'
                request._load_post_and_files()
                request.META['REQUEST_METHOD'] = verb
            setattr(request, verb, request.POST)
        return request

    class Meta:
        queryset = UserProfile.objects.filter()
        resource_name = 'user_profile'
        list_allowed_methods = ['get', 'post']
        detail_allowed_methods = ['get', 'post', 'put', 'delete', 'patch']
        serializer = Serializer()
        authentication = Authentication()
        authorization = DjangoAuthorization()
        always_return_data = True

The References sited above says that the method convert_post_to_VERB() is not intended to handle Multipart data and by patching request._read_started = False. We will be able to upload files using tastypie. But for some-reason even after i did the above mentioned patch i still receive the same "error : You cannot access body after reading from request's data stream".

Kindly help me in resolving the issue, What is it i'm missing ? Or Does anyone have a working logic so that i can take an example.

UPDATE :

  • Put Works after I patched put_detail() method in my resources.
  • I did the same for update_detail() in order to get the PATCH working but it did not work anyway..

Solution

  • You need to define patch_detail not update_detail

    This is how my MultipartResource looks like:

    class MultipartResource(object):
    
        def deserialize(self, request, data, format=None):
    
            if not format:
                format = request.META.get('CONTENT_TYPE', 'application/json')
    
            if format == 'application/x-www-form-urlencoded':
                return request.POST
    
            if format.startswith('multipart/form-data'):
                multipart_data = request.POST.copy()
                multipart_data.update(request.FILES)
                return multipart_data
    
            return super(MultipartResource, self).deserialize(request, data, format)
    
        def put_detail(self, request, **kwargs):
            if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data') and not hasattr(request, '_body'):
                request._body = ''
            return super(MultipartResource, self).put_detail(request, **kwargs)
    
        def patch_detail(self, request, **kwargs):
            if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data') and not hasattr(request, '_body'):
                request._body = ''
            return super(MultipartResource, self).patch_detail(request, **kwargs)
    

    I use it with multi-inheritance togehter with ModelResource on my resources:

    class MyResource(MultipartResource, ModelResource):
        pass
    

    By the way, there is no sense in setting a convert_post_to_VERB method to your resource. Unfortunately, in tastypie's Resource class this is not a method which can be overriden. They define it as a function in the resource.py module.