The request accepted by the APIs
1) GET (list), POST without ID (Use RecipieSerializer, showing only primary key of nested objects)
api/recipe/recipes2) GET (retrive), PUT, PATCH, DELETE with ID (Use RecipeDetailSerializer showing all details of nested objects
api/recipe/recipes/pk
# This serializer points nested objects to its primary keys
class RecipeSerializer(serializers.ModelSerializer):
""" Serializer for Recipe object """
# specify the pointing fields for nested objects
# without these fields, CREATE fails. GET however works
ingredients = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Ingredient.objects.all()
)
tags = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Tag.objects.all()
)
class Meta:
model = Recipe
# ingredients and tag by default refer to its primary keys
fields = ('id', 'title', 'ingredients', 'tags',
'time_miniutes', 'price', 'link')
read_only_fields = ('id',)
# This serializer points its nested object to its own serializer
class RecipeDetailSerializer(RecipeSerializer):
""" Serialize Recipe object """
# Nesting serializers inside serializers
# override nested objects
ingredients = IngredientSerializer(many=True, read_only=True)
tags = TagSerializer(many=True, read_only=True)
# Extend from 'ModelViewSet' to allow urls with id
class RecipeViewSet(viewsets.ModelViewSet):
"""Manage recipes in the database"""
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
serializer_class = serializers.RecipeSerializer
queryset = Recipe.objects.all()
# django allows to change serializers depending on action
# we can have differernt serializer for list and detail views
# for this, we have to override this function
def get_serializer_class(self):
if self.action == 'retrieve':
return serializers.RecipeDetailSerializer
return self.serializer_class
def get_queryset(self):
return self.queryset.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
# write custom code when GET without id is called
def list(self, request):
print("GET without id")
return super().list(request)
# write custom code when GET with ID is called
def retrieve(self, request, pk):
print("GET with id")
return super().retrieve(request, pk)
class PrivateRecipeApiTest(TestCase):
""" Test authenticated recipe api access"""
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
'test@test.com',
'testpass'
)
self.client.force_authenticate(self.user)
# test retrieval of list of recipies
def test_retrieve_recipes(self):
# sample_recipe(user=self.user,
# ingredients=Ingredient.objects.create(user=self.user,
# name='Kale'))
sample_recipe(user=self.user)
ing = Ingredient.objects.create(user=self.user, name='Kale')
rec = sample_recipe(user=self.user)
# this is how we assign data to many to many field
rec.ingredients.add(ing)
# serializer.data
# OrderedDict([('id', 5),
# ('title', 'Sample recipe'),
# ('ingredients', [7]), ('tags', []),
# ('time_miniutes', 10), ('price', '5.00'), ('link', '')]
res = self.client.get(RECIPES_URL)
recipes = Recipe.objects.all().order_by('-id')
serializer = RecipeSerializer(recipes, many=True)
# print(serializer.data)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data, serializer.data)
def test_recipes_limited_to_user(self):
user2 = get_user_model().objects.create_user(
'other@test.com',
'testpass'
)
sample_recipe(self.user)
sample_recipe(user2, title="other")
res = self.client.get(RECIPES_URL)
recipes = Recipe.objects.filter(user=self.user)
serializer = RecipeSerializer(recipes, many=True)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(len(res.data), 1)
self.assertEqual(res.data, serializer.data)
def test_view_recipe_detail(self):
recipe = sample_recipe(user=self.user)
# adding data to many to many field
recipe.tags.add(sample_tag(user=self.user))
recipe.ingredients.add(sample_ingredient(user=self.user))
res = self.client.get(detail_url(recipe.id))
# we dont need many=True as this is a single object
serializer = RecipeDetailSerializer(recipe)
# print(serializer.data)
# {'id': 6,
# 'title': 'Sample recipe',
# 'ingredients': [OrderedDict([('id', 8),
# ('name', 'sample ingredient')])],
# 'tags': [OrderedDict([('id', 2), ('name', 'main course')])],
# 'time_miniutes': 10, 'price': '5.00', 'link': ''}
self.assertEqual(res.data, serializer.data)
# test recipe creation with default params
def test_create_basic_recipe(self):
payload = {
'title': 'choco cheese cake',
'time_miniutes': 30,
'price': 5.00
}
res = self.client.post(RECIPES_URL, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipe = Recipe.objects.get(id=res.data['id'])
# check if each of those payload keys are assigned
for key in payload.keys():
self.assertEqual(payload[key], getattr(recipe, key))
# test recipe creation with Tags
def test_create_recipe_with_tags(self):
tag1 = sample_tag(user=self.user, name="Vegan")
tag2 = sample_tag(user=self.user, name="Dessert")
payload = {
'title': 'choco cheese cake',
'time_miniutes': 30,
'price': 5.00,
'tags': [tag1.id, tag2.id]
}
res = self.client.post(RECIPES_URL, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipe = Recipe.objects.get(id=res.data['id'])
tags = recipe.tags.all()
self.assertEqual(tags.count(), 2)
# assert in is used to check if a value is in a list / queryset
self.assertIn(tag1, tags)
self.assertIn(tag2, tags)
# test recipe creation with ingredients
def test_create_recipe_with_ingredients(self):
ingredient1 = sample_ingredient(user=self.user, name="coco")
ingredient2 = sample_ingredient(user=self.user, name="creme")
payload = {
'title': 'choco cheese cake',
'time_miniutes': 30,
'price': 5.00,
'ingredients': [ingredient1.id, ingredient2.id]
}
res = self.client.post(RECIPES_URL, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipe = Recipe.objects.get(id=res.data['id'])
ingredients = recipe.ingredients.all()
self.assertEqual(ingredients.count(), 2)
# assert in is used to check if a value is in a list / queryset
self.assertIn(ingredient1, ingredients)
self.assertIn(ingredient2, ingredients)
# test partial update recipe (PATCH)
def test_partial_update_recipe(self):
recipe = sample_recipe(user=self.user)
recipe.tags.add(sample_tag(user=self.user))
# now lets replace this tag with a new tag
new_tag = sample_tag(self.user, name="curry")
payload = {
'title': 'indian curry',
'tags': [new_tag.id]
}
self.client.patch(detail_url(recipe.id), payload)
recipe.refresh_from_db()
self.assertEqual(recipe.title, payload['title'])
tags = recipe.tags.all()
self.assertEqual(len(tags), 1)
self.assertIn(new_tag, tags)
# test full update of recipe (PUT)
def test_full_update_recipe(self):
recipe = sample_recipe(user=self.user)
recipe.tags.add(sample_tag(user=self.user))
payload = {
'title': 'new title',
'time_miniutes': 7,
'price': 12.00
}
self.client.put(detail_url(recipe.id), payload)
recipe.refresh_from_db()
# check if each of those payload keys are assigned
for key in payload.keys():
self.assertEqual(payload[key], getattr(recipe, key))
# check if tags is empty
tags = recipe.tags.all()
self.assertEqual(len(tags), 0)
# for delete
# self.client.delete(detail_url(recipe.id))