Uploading and deleting images

Serializer

Create a separate serializer for Recipe model having the image field

# Recipe serializer with image field
class RecipeImageSerializer(serializers.ModelSerializer):
    """serializer for uploading images to recipes"""
    class Meta:
        model = Recipe
        fields = ('id', 'image')
        read_only_fields = ('id',)

Writing custom actions to upload and delete image to a recipe

Custom action upload_image is implemented inside RecipeViewSet using action decorator

1) Specify the append path to url. Appending upload-image to url will look like this

/api/recipe/recipes (POINTS TO RecipeViewSet class List View)
/api/recipe/recipes/<pk> (POINTS to RecipeViewSet class Detail View)
/api/recipe/recipes/<pk>/upload-image (POINTS to custom action with path 'upload-function)

2) Specify if pk arg is needed. If yes, then set detail=True

3) Specify the allowed Methods

we have to encode our own Response. Ie, we have to manually serialize our objects

# for image upload api view
from rest_framework.decorators import action
from rest_framework.response import Response

class RecipeViewSet(viewsets.ModelViewSet):
    ...
    ...
    # writing custom function on call to specific api with specific request
    # This function becomes a custom action = upload_image
    # API CALL (recipe-upload-image) : /api/recipe/recipes/<pk>/upload-image
    @action(methods=['POST', 'DELETE'], detail=True, url_path='upload-image')
    def upload_image(self, request, pk=None):
        # we have to encode our own Response
        # ie, we have to manually serialize our objects
        recipe = self.get_object()
        if(request.method == 'POST'):
            # modify get_serializer_class to consider this action
            serializer = self.get_serializer(recipe, data=request.data)

            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data, status=status.HTTP_200_OK)
            else:
                return Response(serializer.errors,
                                status=status.HTTP_400_BAD_REQUEST)
        elif(request.method == 'DELETE'):
            if recipe.image:
                recipe.image.delete()
                serializer = self.get_serializer(recipe)
                return Response(serializer.data, status=status.HTTP_200_OK)


      def get_serializer_class(self):
        if self.action == 'retrieve':
            return serializers.RecipeDetailSerializer
        elif self.action == 'upload_image':
            return serializers.RecipeImageSerializer

How to use this API (Tests)

POST with file obejct and set format as multipart to the following URL

reverse('recipe:recipe-upload-image', args=[recipe_id])

# for image upload tests
import tempfile
import os
from PIL import Image

# recipies API to POST image (custom action)
# /api/recipe/recipes/1/upload-image
def image_upload_url(recipe_id):
    return reverse('recipe:recipe-upload-image', args=[recipe_id])


class RecipeImageUploadTest(TestCase):

    def setUp(self):
        self.client = APIClient()
        self.user = get_user_model().objects.create_user(
            'test@test.com',
            'testpass'
        )
        self.client.force_authenticate(self.user)
        self.recipe = sample_recipe(user=self.user)

    # destructor. Clean up after test is completed
    def tearDown(self):
        self.recipe.image.delete()

    # test image upload
    def test_upload_image_to_recipe(self):
        # Create a image file object
        with tempfile.NamedTemporaryFile(suffix='.jpg') as ntf:
            # 10x10 black image
            img = Image.new('RGB', (10, 10))
            img.save(ntf, format='JPEG')
            ntf.seek(0)

            # ntf is the file objet which has to to passed to Image field
            res = self.client.post(image_upload_url(self.recipe.id),
                                   {'image': ntf},
                                   format='multipart')

        # at this point ntf is not available in file system
        self.recipe.refresh_from_db()
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertIn('image', res.data)
        self.assertTrue(os.path.exists(self.recipe.image.path))

    # test image upload bad request
    def test_upload_image_bad_request(self):
        res = self.client.post(image_upload_url(self.recipe.id),
                               {'image': 'notimage'},
                               format='multipart')
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)