Skip to content

Commit

Permalink
finalized the recipe API endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
bcg0006 committed Apr 29, 2024
1 parent d0c49f2 commit b04196a
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 3 deletions.
1 change: 1 addition & 0 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'rest_framework',
'drf_spectacular',
'user',
'recipe',
]

MIDDLEWARE = [
Expand Down
1 change: 1 addition & 0 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@
path('api/schema/',SpectacularAPIView.as_view(), name='api-schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='api-schema'), name='api-docs'),
path('api/user/',include('user.urls')),
path('api/recipe/',include('recipe.urls')),
]
3 changes: 2 additions & 1 deletion app/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ class UserAdmin(BaseUserAdmin):
}),
)

admin.site.register(models.User, UserAdmin)
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Recipe)
27 changes: 27 additions & 0 deletions app/core/migrations/0002_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.25 on 2024-04-26 13:26

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


class Migration(migrations.Migration):

dependencies = [
('core', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Recipe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('time_minutes', models.IntegerField()),
('price', models.DecimalField(decimal_places=2, max_digits=5)),
('link', models.CharField(blank=True, max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
19 changes: 17 additions & 2 deletions app/core/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Database models for the core app
"""

from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin

Expand Down Expand Up @@ -39,4 +39,19 @@ class User(AbstractBaseUser, PermissionsMixin):

objects = UserManager()

USERNAME_FIELD = 'email'
USERNAME_FIELD = 'email'

class Recipe(models.Model):
""" Recipe object """
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
time_minutes = models.IntegerField()
price = models.DecimalField(max_digits=5, decimal_places=2)
link = models.CharField(max_length=255, blank=True)

def __str__(self):
return self.title
20 changes: 20 additions & 0 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from django.test import TestCase
from django.contrib.auth import get_user_model
from decimal import Decimal
from core import models

class ModelTests(TestCase):
""" Test the user model """
Expand Down Expand Up @@ -46,3 +48,21 @@ def test_create_new_superuser(self):

self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)

def test_creating_recipe(self):
""" Test creating a recipe is successful """
user = get_user_model().objects.create_user(
'test@example.com',
'test123',
)

recipe = models.Recipe.objects.create(
user=user,
title='Test Recipe',
time_minutes=5,
price=Decimal('5.10'),
description='Test description',
)

self.assertEqual(str(recipe), recipe.title)

Empty file added app/recipe/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions app/recipe/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class RecipeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'recipe'
20 changes: 20 additions & 0 deletions app/recipe/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'''Serializer for our recipe app'''
from rest_framework import serializers
from core.models import Recipe

class RecipeSerializer(serializers.ModelSerializer):
'''Serializer for recipe objects'''

class Meta:
model = Recipe
fields = [
'id', 'title', 'time_minutes', 'price',
'link',
]
read_only_fields = ['id']

class RecipeDetailSerializer(RecipeSerializer):
'''Serializer for recipe detail objects'''

class Meta(RecipeSerializer.Meta):
fields = RecipeSerializer.Meta.fields + ['description']
Empty file added app/recipe/tests/__init__.py
Empty file.
187 changes: 187 additions & 0 deletions app/recipe/tests/test_recipe_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
'''test for recipe model'''
from decimal import Decimal

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

from rest_framework import status
from rest_framework.test import APIClient

from core.models import Recipe

from recipe.serializers import RecipeSerializer, RecipeDetailSerializer

RECIPE_URL = reverse('recipe:recipe-list')

def detail_url(recipe_id):
"""Return recipe detail URL"""
return reverse('recipe:recipe-detail', args=[recipe_id])

def create_recipe(user, **params):
"""Helper function to create a recipe"""
defaults = {
'title': 'Sample recipe',
'time_minutes': 22,
'price': Decimal('5.25'),
'description': 'Sample description',
'link': 'https://sample.com',
}
defaults.update(params)

recipe = Recipe.objects.create(user= user, **defaults)
return recipe

def create_user(**params):
return get_user_model().objects.create_user(**params)

class PublicRecipeApiTests(TestCase):
"""Test unauthenticated recipe API access"""

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

def test_auth_required(self):
"""Test that authentication is required"""
res = self.client.get(RECIPE_URL)

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

class PrivateRecipeApiTests(TestCase):
"""Test authenticated recipe API access"""

def setUp(self):
self.client = APIClient()
self.user = create_user(email='user@example.com', password='test123')
self.client.force_authenticate(self.user)

def test_retrieve_recipes(self):
"""Test retrieving a list of recipes"""
create_recipe(user=self.user)
create_recipe(user=self.user)

res = self.client.get(RECIPE_URL)

recipes = Recipe.objects.all().order_by('-id')
serializer = RecipeSerializer(recipes, many=True)

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

def test_recipes_limited_to_user(self):
"""Test retrieving recipes for user"""
user2 = create_user(email='other@example.com',password='testpass')
create_recipe(user=user2)
create_recipe(user=self.user)

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

def test_view_recipe_detail(self):
"""Test viewing a recipe detail"""
recipe = create_recipe(user=self.user)
#recipe.tags.add(create_tag(user=self.user))
#recipe.ingredients.add(create_ingredient(user=self.user))

url = detail_url(recipe.id)
res = self.client.get(url)

serializer = RecipeDetailSerializer(recipe)
self.assertEqual(res.data, serializer.data)

def test_create_recipe(self):
"Test creating a recipe"
payload = {
'title': 'Chocolate cheesecake',
'time_minutes': 30,
'price': Decimal('5.00'),
}

res = self.client.post(RECIPE_URL, payload)

self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipe = Recipe.objects.get(id=res.data['id'])
for k,v in payload.items():
self.assertEqual(v, getattr(recipe, k))
self.assertEqual(recipe.user, self.user)

def test_partial_update_recipe(self):
"""Test updating a recipe with patch"""
original_link = 'https://sample.com/recipedetail'
recipe = create_recipe(user=self.user, title= 'Sample recipe', link=original_link)

payload = {'title': 'New recipe title'}
url = detail_url(recipe.id)
res = self.client.patch(url, payload)

self.assertEqual(res.status_code, status.HTTP_200_OK)
recipe.refresh_from_db()
self.assertEqual(recipe.title, payload['title'])
self.assertEqual(recipe.link, original_link)

def test_full_update_recipe(self):
"Test full updated recipe"
recipe = create_recipe(
user=self.user, title='Pennes alla Norma',
time_minutes=212,
price=Decimal('50.25'),
description='Sample description1',
link='https://sample.com')

payload = {
'title': 'Spaghetti Carbonara',
'time_minutes': 25,
'price': Decimal('5.00'),
'description': 'Sample description2',
'link': 'https://sample2.com'
}

url = detail_url(recipe.id)
res = self.client.patch(url, payload)
self.assertEqual(res.status_code, status.HTTP_200_OK)
recipe.refresh_from_db()
for k,v in payload.items():
self.assertEqual(v, getattr(recipe, k))
self.assertEqual(recipe.user, self.user)

def test_update_recipe_by_other_user(self):
"""Test changing the recipe user result in an error"""
user2 = create_user(email='user2@example2.com', password='testpass')
recipe = create_recipe(user=self.user)

payload = {'user': user2.id}
url = detail_url(recipe.id)
self.client.patch(url, payload)

recipe.refresh_from_db()
self.assertEqual(recipe.user, self.user)

def test_delete_recipe(self):
"""Test deleting a recipe"""
recipe = create_recipe(user=self.user)
url = detail_url(recipe.id)
res = self.client.delete(url)

self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Recipe.objects.filter(id=recipe.id).count(), 0)

def test_delete_recipe_by_other_user(self):
"""Test deleting a recipe by other user"""
user2 = create_user(email='user2@example.com', password='testpass')
recipe = create_recipe(user=user2)
url = detail_url(recipe.id)
res = self.client.delete(url)

self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(Recipe.objects.filter(id=recipe.id).exists())






17 changes: 17 additions & 0 deletions app/recipe/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
URL mapping for recipe app
"""

from django.urls import path, include
from rest_framework.routers import DefaultRouter

from recipe import views

router = DefaultRouter()
router.register('recipes', views.RecipeViewSet)

app_name = 'recipe'

urlpatterns = [
path('', include(router.urls)),
]
31 changes: 31 additions & 0 deletions app/recipe/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
views for recipe app
"""
from rest_framework import viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated

from core.models import Recipe
from recipe import serializers

class RecipeViewSet(viewsets.ModelViewSet):
"""Manage recipes in the database"""
serializer_class = serializers.RecipeSerializer
queryset = Recipe.objects.all()
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]

def get_queryset(self):
"""Return objects for the current authenticated user only"""
return self.queryset.filter(user=self.request.user).order_by('-id')

def get_serializer_class(self):
"""Return the serializer class for request"""
if self.action == 'list':
return serializers.RecipeSerializer

return serializers.RecipeDetailSerializer

def perform_create(self, serializer):
"""Create a new recipe"""
serializer.save(user=self.request.user)

0 comments on commit b04196a

Please sign in to comment.