Ingredients endpoint

We ll create a another table called Ingredients.

It will be exactly similar to Tags model

Create Ingredient model

1) Add Ingredient Model

# TEST
# sample user for the tests
def sample_user(email="test@test.com", password="testpass"):
    return get_user_model().objects.create(email=email, password=password)


class IngredientModelTests(TestCase):

    # test the tag usage
    def test_ingredient_str(self):
        # only include mandatory fields here
        ingredient = models.Ingredient.objects.create(
            user=sample_user(),
            name="Vegan"
        )

        self.assertEqual(str(ingredient), ingredient.name)

# MODEL
class Ingredient(models.Model):
    """ Ingredient to be used in a recipe"""
    name = models.CharField(max_length=255)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )

    def __str__(self):
        return self.name

2) Make migrations

3) Register model in admin.py

Tests for Ingredients API

/api/recepi/ingredients should allow creation and retrieval of list of ingredients pertaining to authenticated user

The tests are eactly similar to tests of Tags API

from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase

from rest_framework import status
from rest_framework.test import APIClient
from core.models import Ingredient
from recipe.serializers import IngredientSerializer

INGREDIENT_URL = reverse("recipe:ingredient-list")


class PublicIngredientsApiTests(TestCase):
    """ Test publicly available ingredients API"""

    def setUp(self):
        self.client = APIClient()

    # test if ingredients api required authentication
    def test_login_required(self):
        res = self.client.get(INGREDIENT_URL)

        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)


class PrivateIngredientsApiTest(TestCase):
    """ Test  private ingredients api"""

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

    # test if ingredients can be retrieved
    def test_retrieve_ingredients_list(self):
        Ingredient.objects.create(user=self.user, name='Kale')
        Ingredient.objects.create(user=self.user, name='Salt')

        res = self.client.get(INGREDIENT_URL)

        ingredients = Ingredient.objects.all().order_by('-name')
        serializer = IngredientSerializer(ingredients, many=True)

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)

    # test if ingredients returned are limited to the auth user
    def test_ingredients_limited_to_user(self):
        user2 = get_user_model().objects.create_user(
            "other@test.com",
            "testpass"
        )

        ingred = Ingredient.objects.create(user=self.user, name="auth user")
        Ingredient.objects.create(user=user2, name="other user")

        res = self.client.get(INGREDIENT_URL)

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(len(res.data), 1)
        self.assertEqual(res.data[0]['name'], ingred.name)

    # test ingredient creation api
    def test_create_ingredient_successful(self):
        payload = {'name': 'cucumber'}
        res = self.client.post(INGREDIENT_URL, payload)

        self.assertEqual(res.status_code, status.HTTP_201_CREATED)

        exists = Ingredient.objects.filter(
            user=self.user,
            name=payload['name']
        ).exists()

        self.assertTrue(exists)

    # test if invalid ingredients are not created
    def test_create_ingredient_invalid(self):
        res = self.client.post(INGREDIENT_URL, {'name': ''})
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

Create Serializer

class IngredientSerializer(serializers.ModelSerializer):
    """ Serializer for ingredient object"""

    class Meta:
        model = Ingredient
        fields = ('id', 'name')
        read_only_fields = ('id',)

Create view set

This view also looks similar to TagsViewSet. So we create a common base class

from rest_framework import viewsets, mixins
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated

from core.models import Tag, Ingredient
from recipe import serializers


# viewset is used when a separate url is mapped to this view for each user
# /api/recepi/tags/ : mapped separately for each user
class BaseRecepiAttr(viewsets.GenericViewSet,
                     mixins.ListModelMixin,
                     mixins.CreateModelMixin):

    authentication_classes = (TokenAuthentication,)
    permission_classes = (IsAuthenticated,)

    # abstract attributes
    @property
    def serializer_class(self):
        raise NotImplementedError

    # ListModelMixin returns a list of objects when GET is called
    @property
    def queryset(self):
        raise NotImplementedError

    # This method should be overriden
    # if we dont want to modify query set based on current instance attributes
    def get_queryset(self):
        return self.queryset.filter(user=self.request.user).order_by('-name')

    # override this method for CreateModelMixin
    # create operation is done here (unlike in UserModelSerializer)
    # because serializer can not have user
    # we pass user to serializer and save it
    def perform_create(self, serializer):
        serializer.save(user=self.request.user)


# This class looks almost similar to previous class.
# Its better to write a common base class
# /api/recepi/ingredients/ : mapped separately for each user
class IngredientViewSet(BaseRecepiAttr):

    serializer_class = serializers.IngredientSerializer
    queryset = Ingredient.objects.all()

Register router in recepi urls

router.register('ingredients', views.IngredientViewSet)
# reverse url is 'recepi:ingredient-list'