-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
329 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ | |
'rest_framework', | ||
'drf_spectacular', | ||
'user', | ||
'recipe', | ||
] | ||
|
||
MIDDLEWARE = [ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
|
||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |