Skip to content

Commit

Permalink
Added ingredients API
Browse files Browse the repository at this point in the history
  • Loading branch information
belloibrahv committed Jun 4, 2024
1 parent 91368fd commit 719f48d
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 20 deletions.
1 change: 1 addition & 0 deletions app/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ class UserAdmin(BaseUserAdmin):
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Recipe)
admin.site.register(models.Tag)
admin.site.register(models.Ingredient)
28 changes: 28 additions & 0 deletions app/core/migrations/0005_auto_20240527_1720.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.25 on 2024-05-27 17:20

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('core', '0004_rename_tag_recipe_tags'),
]

operations = [
migrations.CreateModel(
name='Ingredient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='recipe',
name='ingredients',
field=models.ManyToManyField(to='core.Ingredient'),
),
]
16 changes: 15 additions & 1 deletion app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


class UserManager(BaseUserManager):
"""Manager of users"""
"""Manager of users."""

def create_user(self, email, password=None, **extra_args):
"""create, save and return a new user"""
Expand Down Expand Up @@ -49,6 +49,7 @@ def __str__(self):


class Recipe(models.Model):
"""Model for recipe."""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
Expand All @@ -59,6 +60,7 @@ class Recipe(models.Model):
price = models.DecimalField(max_digits=5, decimal_places=2)
link = models.CharField(max_length=255, blank=True)
tags = models.ManyToManyField('Tag')
ingredients = models.ManyToManyField('Ingredient')

def __str__(self):
return self.title
Expand All @@ -74,3 +76,15 @@ class Tag(models.Model):

def __str__(self):
return self.name


class Ingredient(models.Model):
"""Model for ingredient."""
name = models.CharField(max_length=255)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)

def __str__(self):
return self.name
10 changes: 10 additions & 0 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,13 @@ def test_create_tag(self):
tag = models.Tag.objects.create(user=user, name='Tag1')

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

def test_create_ingredient(self):
"""Test creating an ingredient is successful."""
user = create_user()
ingredient = models.Ingredient.objects.create(
user=user,
name="Ingredient1"
)

self.assertEqual(str(ingredient), ingredient.name)
33 changes: 25 additions & 8 deletions app/recipe/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from rest_framework import serializers
from core.models import Recipe, Tag
from core.models import Recipe, Tag, Ingredient


class IngredientSerializer(serializers.ModelSerializer):
"""Serializer for ingredients."""
class Meta:
model = Ingredient
fields = ('id', 'name')
read_only_fields = ('id',)


class TagSerializer(serializers.ModelSerializer):
"""Serializer for tags."""

class Meta:
model = Tag
fields = ('id', 'name')
Expand All @@ -14,10 +21,12 @@ class Meta:
class RecipeSerializer(serializers.ModelSerializer):
"""Serializer for recipes."""
tags = TagSerializer(many=True, required=False)
ingredients = IngredientSerializer(many=True, required=False)

class Meta:
model = Recipe
fields = ('id', 'title', 'time_minutes', 'price', 'link', 'tags')
fields = ('id', 'title', 'time_minutes',
'price', 'link', 'tags', 'ingredients')
read_only_fields = ('id',)

def _get_or_create_tag(self, tags, recipe):
Expand All @@ -27,28 +36,36 @@ def _get_or_create_tag(self, tags, recipe):
user=self.context['request'].user, **tag,)
recipe.tags.add(tag_obj)

def _get_or_create_ingredient(self, ingredients, recipe):
"""Handle getting and creating ingredients as needed."""
for ingredient in ingredients:
ingredient_obj, created = Ingredient.objects.get_or_create(
user=self.context['request'].user, **ingredient,
)
recipe.ingredients.add(ingredient_obj)

def create(self, validated_data):
"""Create a recipe."""
tags = validated_data.pop('tags', [])
ingredients = validated_data.pop('ingredients', [])
recipe = Recipe.objects.create(**validated_data)

self._get_or_create_tag(tags, recipe)

self._get_or_create_ingredient(ingredients, recipe)
return recipe

def update(self, instance, validated_data):
"""Update recipe."""
tags = validated_data.pop('tags', [])
ingredients = validated_data.pop('ingredients', [])
instance = super().update(instance, validated_data)
instance.tags.clear()

instance.ingredients.clear()
self._get_or_create_tag(tags, instance)

self._get_or_create_ingredient(ingredients, instance)
return instance


class RecipeDetailSerializer(RecipeSerializer):
"""Serializer for recipe detail."""

class Meta(RecipeSerializer.Meta):
fields = RecipeSerializer.Meta.fields + ('description',)
99 changes: 99 additions & 0 deletions app/recipe/tests/test_ingredients_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
Tests for the ingredients 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')


def detail_url(ingredient_id):
"""Create and return ingredient detail id."""
return reverse('recipe:ingredient-detail', args=[ingredient_id])


def create_user(email='[email protected]', password='testuser123'):
"""Create and return a user."""
return get_user_model().objects.create_user(email=email, password=password)


class PublicIngredientApiTest(TestCase):
"""Test unauthorized API requests."""

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

def test_auth_required(self):
"""Test authentication required to list ingredients."""
res = self.client.get(INGREDIENT_URL)

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


class PrivateIngredientApiTest(TestCase):
"""Test unauthenticated API requests."""

def setUp(self):
self.user = create_user()
self.client = APIClient()
self.client.force_authenticate(self.user)

def test_retrieve_ingredient(self):
"""Test retrieving a list of ingredients."""
Ingredient.objects.create(user=self.user, name='Kale')
Ingredient.objects.create(user=self.user, name='Vanilla')

res = self.client.get(INGREDIENT_URL)

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

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

def test_ingredient_limited_to_user(self):
"""Test list of ingredients limited to authenticated user."""

user2 = create_user(email='[email protected]')
Ingredient.objects.create(user=user2, name='Salt')
ingredient = Ingredient.objects.create(user=self.user, name='Pepper')

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'], ingredient.name)
self.assertEqual(res.data[0]['id'], ingredient.id)

def test_update_ingredient(self):
"""Test updating ingredient."""
ingredient = Ingredient.objects.create(
user=self.user, name='Jollof Rice')

payload = {'name': 'Nigerian Jollof Rice'}
url = detail_url(ingredient.id)
res = self.client.patch(url, payload)

self.assertEqual(res.status_code, status.HTTP_200_OK)
ingredient.refresh_from_db()
self.assertEqual(ingredient.name, payload['name'])

def test_delete_ingredient(self):
"""Test deleting ingredient."""
ingredient = Ingredient.objects.create(
user=self.user, name='Egusi Soup')

url = detail_url(ingredient.id)
res = self.client.delete(url)

self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
ingredients = Ingredient.objects.filter(user=self.user)
self.assertFalse(ingredients.exists())
104 changes: 103 additions & 1 deletion app/recipe/tests/test_recipe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from rest_framework import status
from rest_framework.test import APIClient

from core.models import (Recipe, Tag)
from core.models import (Recipe, Tag, Ingredient)
from recipe.serializers import (RecipeSerializer, RecipeDetailSerializer)

RECIPES_URL = reverse('recipe:recipe-list')
Expand Down Expand Up @@ -196,6 +196,7 @@ def test_create_recipe_with_new_tags(self):
self.assertTrue(exists)

def test_create_recipe_with_existing_tags(self):
"""Test creating recipe with an existing tag."""
tag_indian = Tag.objects.create(user=self.user, name='Indian')
payload = {
'title': 'Pongal',
Expand Down Expand Up @@ -263,3 +264,104 @@ def test_clear_tags(self):

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(recipe.tags.count(), 0)

def test_create_recipe_with_new_ingredients(self):
"""Test creating recipe with new ingredients."""
payload = {
'title': 'Thai Prawn curry',
'time_minutes': 30,
'price': Decimal('2.50'),
'tags': [{'name': 'Thai'}, {'name': 'Dinner'}],
'ingredients': [{'name': 'Pepper'}, {'name': 'Salt'}],
}
res = self.client.post(RECIPES_URL, payload, format='json')

self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipes = Recipe.objects.filter(user=self.user)
self.assertEqual(recipes.count(), 1)
recipe = recipes[0]
self.assertEqual(recipe.ingredients.count(), 2)
for ingredient in payload['ingredients']:
exists = recipe.ingredients.filter(
name=ingredient['name'],
user=self.user,
).exists()
self.assertTrue(exists)

def test_recipe_with_existing_ingredient(self):
"""Test creating recipe with an existing ingredient."""
ingredient_salt = Ingredient.objects.create(
user=self.user, name='Salt')
payload = {
'title': 'Jollof Rice',
'time_minutes': 40,
'price': Decimal('10.00'),
'tags': [
{'name': 'Nigerian'},
{'name': 'Lunch'}
],
'ingredients': [{'name': 'Salt'}, {'name': 'Pepper'}],
}

res = self.client.post(RECIPES_URL, payload, format='json')

self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipes = Recipe.objects.all()
self.assertEqual(recipes.count(), 1)
recipe = recipes[0]
ingredients = recipe.ingredients.all()
self.assertEqual(ingredients.count(), 2)
self.assertIn(ingredient_salt, ingredients)
for ingredient in payload['ingredients']:
exists = recipe.ingredients.filter(
name=ingredient['name'],
user=self.user,
).exists()
self.assertTrue(exists)

def test_create_ingredient_on_update(self):
"""Test creating ingredient on recipe update."""
recipe = create_recipe(user=self.user)

payload = {'ingredients': [{'name': 'Fish'}]}
url = detail_url(recipe.id)
res = self.client.patch(url, payload, format='json')

self.assertEqual(res.status_code, status.HTTP_200_OK)
new_ingredient = Ingredient.objects.get(
name="Fish",
user=self.user,
)
self.assertIn(new_ingredient, recipe.ingredients.all())

def test_update_assign_ingredient(self):
"""Test assigning existing ingredient when updating recipe."""
maggi_ingredient = Ingredient.objects.create(
user=self.user, name='Maggi')
recipe = create_recipe(user=self.user)
recipe.ingredients.add(maggi_ingredient)

curry_ingredient = Ingredient.objects.create(
user=self.user, name='Curry')
payload = {'ingredients': [{'name': 'Curry'}]}
url = detail_url(recipe.id)

res = self.client.patch(url, payload, format='json')

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertIn(curry_ingredient, recipe.ingredients.all())
self.assertNotIn(maggi_ingredient, recipe.ingredients.all())

def test_clear_recipe_ingredient(self):
"""Test clearing recipe ingredient."""
ingredient = Ingredient.objects.create(user=self.user, name='Salt')
recipe = create_recipe(user=self.user)
recipe.ingredients.add(ingredient)

payload = {'ingredients': []}
url = detail_url(recipe.id)

res = self.client.patch(url, payload, format='json')

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(recipe.ingredients.count(), 0)
Loading

0 comments on commit 719f48d

Please sign in to comment.