Skip to content

Commit

Permalink
added recipe API
Browse files Browse the repository at this point in the history
  • Loading branch information
rotem123456 committed Jul 24, 2024
1 parent 1fb6f1f commit 600c340
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 10 deletions.
1 change: 1 addition & 0 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'rest_framework.authtoken',
'drf_spectacular',
'user',
'recipe'
]

MIDDLEWARE = [
Expand Down
3 changes: 2 additions & 1 deletion app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@
SpectacularSwaggerView.as_view(url_name='api-schema'),
name='api-docs',
),
path('api/user/', include('user.urls'))
path('api/user/', include('user.urls')),
path('api/recipe/', include('recipe.urls')),
]
15 changes: 10 additions & 5 deletions app/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
"""
Django admin customization.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _

from core import models


class UserAdmin(BaseUserAdmin):
"""Define the admin pages for users."""
ordering = ['id']
list_display = ['email', 'name', 'first_name', 'last_name']
list_display = ['email', 'name']
fieldsets = (
(None, {'fields': ('email', 'password')}),
(_('Personal Info'), {'fields': ('name', 'first_name', 'last_name')}),
(_('Personal Info'), {'fields': ('name',)}),
(
_('Permissions'),
{
Expand All @@ -31,13 +36,13 @@ class UserAdmin(BaseUserAdmin):
'password1',
'password2',
'name',
'first_name',
'last_name',
'is_active',
'is_staff',
'is_superuser',
),
}),
)

admin.site.register(models.User, UserAdmin)

admin.site.register(models.User, UserAdmin)
admin.site.register(models.Recipe)
21 changes: 21 additions & 0 deletions app/core/migrations/0002_auto_20240718_2012.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.25 on 2024-07-18 20:12

from django.db import migrations


class Migration(migrations.Migration):

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

operations = [
migrations.RemoveField(
model_name='user',
name='first_name',
),
migrations.RemoveField(
model_name='user',
name='last_name',
),
]
26 changes: 26 additions & 0 deletions app/core/migrations/0003_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.25 on 2024-07-19 13:37

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


class Migration(migrations.Migration):

dependencies = [
('core', '0002_auto_20240718_2012'),
]

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)),
('price', models.DecimalField(decimal_places=2, max_digits=5)),
('description', models.TextField(blank=True)),
('link', models.CharField(blank=True, max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
18 changes: 18 additions & 0 deletions app/core/migrations/0004_recipe_time_minutes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2024-07-19 16:07

from django.db import migrations, models


class Migration(migrations.Migration):

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

operations = [
migrations.AddField(
model_name='recipe',
name='time_minutes',
field=models.IntegerField(default=0),
),
]
18 changes: 16 additions & 2 deletions app/core/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils import timezone
from django.conf import settings

class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
Expand All @@ -25,8 +26,6 @@ def create_superuser(self, email, password, **extra_fields):

class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(max_length=254, unique=True)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
name = models.CharField(max_length=255,blank=True) # Add this line
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
Expand All @@ -36,3 +35,18 @@ class User(AbstractBaseUser, PermissionsMixin):

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name'] # Add this line

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

def __str__(self):
return self.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'
22 changes: 22 additions & 0 deletions app/recipe/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Serializers for recipe APIs
"""
from rest_framework import serializers

from core.models import Recipe


class RecipeSerializer(serializers.ModelSerializer):
"""Serializer for recipes."""

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


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

class Meta(RecipeSerializer.Meta):
fields = RecipeSerializer.Meta.fields + ['description']
Empty file added app/recipe/tests/__init__.py
Empty file.
196 changes: 196 additions & 0 deletions app/recipe/tests/test_recipe_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""
Tests for recipe APIs.
"""
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
)


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

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

def create_recipe(user, **params):
"""Create and return a sample recipe."""
defaults = {
'title': 'Sample recipe title',
'time_minutes': 22,
'price': Decimal('5.25'),
'description': 'Sample description',
'link': 'http://example.com/recipe.pdf',
}
defaults.update(params)

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


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

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

def test_auth_required(self):
"""Test auth is required to call API."""
res = self.client.get(RECIPES_URL)

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

def create_user(**params):
"""Create and return a new user."""
return get_user_model().objects.create_user(**params)

class PrivateRecipeApiTests(TestCase):
"""Test authenticated API requests."""

def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
'user@example.com',
'testpass123',
)
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(RECIPES_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_recipe_list_limited_to_user(self):
"""Test list of recipes is limited to authenticated user."""
other_user = create_user(email = 'other@example.com',password = 'password123')
create_recipe(user=other_user)
create_recipe(user=self.user)

res = self.client.get(RECIPES_URL)

recipes = Recipe.objects.filter(user=self.user)
serializer = RecipeSerializer(recipes, many=True)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data, serializer.data)

def test_get_recipe_detail(self):
"""Test get recipe detail."""
recipe = create_recipe(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 create recipe."""
payload = {
'title': 'Test Recipe',
'time_minutes':30,
'price': Decimal('5.99')
}
res = self.client.post(RECIPES_URL, payload)
self.assertEqual(res.status_code,status.HTTP_201_CREATED)
recipe = Recipe.objects.get(id=res.data['id'])
for key, val in payload.items():
self.assertEqual(getattr(recipe,key),val)
self.assertEqual(recipe.user,self.user)

def test_partial_update(self):
"""Test partial update of a recipe"""
original_link = 'https://example.com/recipe.pdf'
recipe = create_recipe(
user = self.user,
title = 'Sample recipe title',
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)
self.assertEqual(recipe.user, self.user)

def test_full_update(self):
"""Test full update of the recipe"""
recipe = create_recipe(
user = self.user,
title = 'Recipe Before Changed',
link = 'https://example.com/recipe.pdf',
time_minutes = 30,
description = 'Description before changed',
price = Decimal('1.70')

)
url = detail_url(recipe.id)
payload = {
'title':'Recipe After Changed',
'link': 'https://example.com/recipe_new.pdf',
'time_minutes': 29,
'description': 'Description after changed',
'price': Decimal('2.50'),
}
res = self.client.put(url,payload)
recipe.refresh_from_db()
self.assertEqual(res.status_code, status.HTTP_200_OK)
for key, val in payload.items():
self.assertEqual(getattr(recipe,key),val)
self.assertEqual(recipe.user,self.user)

def test_update_user_returns_error(self):
"""Test changing the recipe user results in an error."""
new_user = create_user(email = 'test@example.com',password = 'password123')
recipe = create_recipe(
user = self.user,
)
payload = {'user': new_user.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 successful."""
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.assertFalse(Recipe.objects.filter(id=recipe.id).exists())

def test_recipe_other_users_recipe_error(self):
"""Test trying to delete another users recipe gives error."""
new_user = create_user(email='user2@example.com', password='test123')
recipe = create_recipe(user=new_user)

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())
Loading

0 comments on commit 600c340

Please sign in to comment.