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',)
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
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)