Skip to content

Commit

Permalink
Update: Implemented the recipe Ingredient API
Browse files Browse the repository at this point in the history
  • Loading branch information
PC-Ngumoha committed Mar 15, 2024
1 parent c262126 commit deea582
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 15 deletions.
1 change: 1 addition & 0 deletions app/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,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/0004_auto_20240314_1623.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.25 on 2024-03-14 16:23

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


class Migration(migrations.Migration):

dependencies = [
('core', '0003_auto_20240313_2235'),
]

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'),
),
]
14 changes: 14 additions & 0 deletions app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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):
"""String representation of recipe object."""
Expand All @@ -76,3 +77,16 @@ class Tag(models.Model):
def __str__(self):
"""String representation of a tag"""
return self.name


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

def __str__(self):
"""String representation."""
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 @@ -79,3 +79,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):
"""Tests creating an ingredient is successful"""
user = create_user()
ingredient = models.Ingredient.objects.create(
user=user,
name='Ingredient 1'
)

self.assertEqual(str(ingredient), ingredient.name)
37 changes: 32 additions & 5 deletions app/recipe/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,40 @@
"""
from rest_framework import serializers

from core.models import (Recipe, Tag)
from core.models import (Recipe, Tag, Ingredient)


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

class Meta:
model = Tag
fields = ['id', 'name']
read_only_fields = ['id']


class IngredientSerializer(serializers.ModelSerializer):
"""Serializer for ingredients."""

class Meta:
model = Ingredient
fields = ['id', 'name']
read_only_fields = ['id']


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']
'tags', 'ingredients']
read_only_fields = ['id']

def _get_or_create_tags(self, tags, recipe):
"""Handle gettinr of creating tags."""
"""Handle getting or creating tags."""
auth_user = self.context['request'].user
for tag in tags:
tag_obj, _ = Tag.objects.get_or_create(
Expand All @@ -35,22 +45,39 @@ def _get_or_create_tags(self, tags, recipe):
)
recipe.tags.add(tag_obj)

def _get_or_create_ingredients(self, ingredients, recipe):
"""Handle getting or creating ingredients."""
auth_user = self.context['request'].user
for ingredient in ingredients:
ingredient_obj, _ = Ingredient.objects.get_or_create(
user=auth_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_tags(tags, recipe)
self._get_or_create_ingredients(ingredients, recipe)

return recipe

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

if tags is not None:
instance.tags.clear()
self._get_or_create_tags(tags, instance)

if ingredients is not None:
instance.ingredients.clear()
self._get_or_create_ingredients(ingredients, instance)

for attr, value in validated_data.items():
setattr(instance, attr, value)

Expand Down
97 changes: 97 additions & 0 deletions app/recipe/tests/test_ingredients_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
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


INGREDIENTS_URL = reverse('recipe:ingredient-list')


def create_user(email='user@example.com', password='testpass123'):
"""Create and return user."""
return get_user_model().objects.create_user(email, password)


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


class PublicIngredientsAPITests(TestCase):
"""Tests for unauthenticated requests to the API."""

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

def test_auth_required(self):
"""Tests that auth is required for retrieving ingredients."""
res = self.client.get(INGREDIENTS_URL)

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


class PrivateIngredientsAPITests(TestCase):
"""Tests for authenticated requests to the API."""

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

def test_retrieve_ingredients(self):
"""Tests retrieving ingredients."""
Ingredient.objects.create(user=self.user, name='Kale')
Ingredient.objects.create(user=self.user, name='Vanilla')

res = self.client.get(INGREDIENTS_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)

def test_ingredients_limited_to_user(self):
"""Tests list of ingredients is limited to auth user."""
user2 = create_user(email='user2@example.com')
Ingredient.objects.create(user=user2, name='Salt')
ingredient = Ingredient.objects.create(user=self.user, name='Pepper')

res = self.client.get(INGREDIENTS_URL)

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

def test_update_ingredient(self):
"""Test updating an ingredient"""
ingredient = Ingredient.objects.create(user=self.user,
name='Cilantra')
payload = {'name': 'Coriander'}
url = detail_url(ingredient.id)
res = self.client.patch(url, payload, format='json')

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

def test_delete_ingredient(self):
"""Test deleting an ingredient"""
ingredient = Ingredient.objects.create(user=self.user,
name='Lettuce')
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())
95 changes: 94 additions & 1 deletion app/recipe/tests/test_recipe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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,
Expand Down Expand Up @@ -287,3 +287,96 @@ def test_clear_recipe_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):
"""Tests creating a recipe with new ingredients."""
payload = {
'title': 'Cauliflower Tacos',
'time_minutes': 60,
'price': Decimal('4.30'),
'ingredients': [{'name': 'Cauliflower'}, {'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.get('ingredients'):
exists = recipe.ingredients.filter(
name=ingredient.get('name'),
user=self.user,
).exists()
self.assertTrue(exists)

def test_create_recipe_with_existing_ingredients(self):
"""Test creating a new recipe with existing ingredients."""
ingredient = Ingredient.objects.create(user=self.user,
name='Lemon')
payload = {
'title': 'Vietnamese Soup',
'time_minutes': 25,
'price': Decimal('2.55'),
'ingredients': [{'name': 'Lemon'}, {'name': 'Fish Sauce'}],
}
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)
self.assertIn(ingredient, recipe.ingredients.all())
for ingredient in payload.get('ingredients'):
exists = recipe.ingredients.filter(
name=ingredient.get('name'),
user=self.user
).exists()
self.assertTrue(exists)

def test_create_ingredient_on_update(self):
"""Tests creating ingredient when updating recipe."""
recipe = create_recipe(user=self.user)

payload = {'ingredients': [{'name': 'Limes'}]}
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(user=self.user,
name='Limes')
self.assertIn(new_ingredient, recipe.ingredients.all())

def test_update_recipe_assign_ingredient(self):
"""Tests assigning an existing ingredient when updating a recipe.
"""
ingredient1 = Ingredient.objects.create(user=self.user,
name='Pepper')
recipe = create_recipe(user=self.user)
recipe.ingredients.add(ingredient1)

ingredient2 = Ingredient.objects.create(user=self.user,
name='Chili')
payload = {'ingredients': [{'name': 'Chili'}]}
url = detail_url(recipe.id)
res = self.client.patch(url, payload, format='json')

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertIn(ingredient2, recipe.ingredients.all())
self.assertNotIn(ingredient1, recipe.ingredients.all())


def test_clear_recipe_ingredients(self):
"""Tests clearing a recipe's ingredients"""
ingredient = Ingredient.objects.create(user=self.user,
name='Garlic')
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)
1 change: 1 addition & 0 deletions app/recipe/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
router = DefaultRouter()
router.register('recipes', views.RecipeViewSet)
router.register('tags', views.TagViewSet)
router.register('ingredients', views.IngredientViewSet)

app_name = 'recipe'

Expand Down
Loading

0 comments on commit deea582

Please sign in to comment.