Skip to content

Commit

Permalink
update ingredient API and ingredient tests
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelusc committed Apr 22, 2024
1 parent 93fe871 commit cfbf6d8
Show file tree
Hide file tree
Showing 10 changed files with 451 additions and 13 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/0004_auto_20240421_1731.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.25 on 2024-04-21 17:31

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


class Migration(migrations.Migration):

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

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 @@ -57,6 +57,8 @@ 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') # many-to-many relationship
ingredients = models.ManyToManyField(
'Ingredient') # many-to-many relationship

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

def __str__(self):
return self.name


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
8 changes: 8 additions & 0 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,11 @@ 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)
82 changes: 72 additions & 10 deletions app/recipe/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,90 @@
from core.models import (
Recipe,
Tag,
Ingredient,
)


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


class TagSerializer(serializers.ModelSerializer):
"""Serializer for tag objects"""

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


class RecipeSerializer(serializers.ModelSerializer):
"""Serializer for recipe objects"""
tags = TagSerializer(
many=True, required=False) # many=True because it's a nested object
ingredients = IngredientSerializer(many=True, required=False)

class Meta:
model = Recipe
fields = ['id', 'title', 'time_minutes', 'price', 'link']
# tags is a nested object, so we need to include it in the fields
fields = ['id', 'title', 'time_minutes',
'price', 'link', 'tags', 'ingredients']
read_only_fields = ['id']

def _get_or_create_tags(self, tags, recipe):
"""Handle getting or creating tags as needed"""
auth_user = self.context['request'].user
for tag in tags:
tag_obj, created = Tag.objects.get_or_create(
user=auth_user,
**tag, # name = tag['name']
)
recipe.tags.add(tag_obj)

# _ means it's a internal method and should not be called directly
def _get_or_create_ingredients(self, ingredients, recipe):
"""Handle getting or creating ingredients as needed"""
auth_user = self.context['request'].user
for ingredient in ingredients:
ingredient_obj, created = Ingredient.objects.get_or_create(
user=auth_user,
**ingredient,
)
recipe.ingredients.add(ingredient_obj)

def create(self, validated_data):
"""Create a new 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', 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)

instance.save()
return instance


class RecipeDetailSerializer(RecipeSerializer):
"""Serializer for recipe detail view"""
class Meta(RecipeSerializer.Meta):
fields = RecipeSerializer.Meta.fields + ['description']


class TagSerializer(serializers.ModelSerializer):
"""Serializer for tag objects"""

class Meta:
model = Tag
fields = ['id', 'name']
read_only_fields = ['id']
95 changes: 95 additions & 0 deletions app/recipe/tests/test_ingredients_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
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 detail_url(ingredient_id):
"""Create and return an ingredient detail URL"""
return reverse('recipe:ingredient-detail', args=[ingredient_id])


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


class PublicIngredientsApiTests(TestCase):
"""Test unauthenticated API requests"""

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

def test_login_required(self):
"""Test auth is required for retrieving ingredients"""
response = self.client.get(INGREDIENTS_URL)

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


class PrivateIngredientsApiTests(TestCase):
"""Test authenticated API requests"""

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

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

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):
"""Test list of ingredients is limited to the authenticated user"""
user2 = create_user(email="[email protected]")
Ingredient.objects.create(user=user2, name='Vinegar')
ingredient = Ingredient.objects.create(user=self.user, name='Tumeric')

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

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

payload = {'name': 'Cilantro'}
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_create_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())
Loading

0 comments on commit cfbf6d8

Please sign in to comment.