Recipe end point

The request accepted by the APIs

1) GET (list), POST without ID (Use RecipieSerializer, showing only primary key of nested objects)

api/recipe/recipes

2) GET (retrive), PUT, PATCH, DELETE with ID (Use RecipeDetailSerializer showing all details of nested objects

api/recipe/recipes/pk

Serializers

# 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)

Viewset

# 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)

Using the API (Test cases)

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