diff --git a/README2.md b/README2.md index 4f1e177..9647702 100644 --- a/README2.md +++ b/README2.md @@ -115,3 +115,171 @@ Migrations for 'core': core/migrations/0002_recipe.py - Create model Recipe ``` + +## Creación de app recipe + +`docker compose run --rm app sh -c "python manage.py startapp recipe"` + +```sh +[+] Creating 1/0 + ✔ Container recipes_api_django-db-1 Running 0.0s +``` + +- Se eliminan, directorio `migrations`, `test.py`, `admin.py` `models.py` +- Se crean directorio `reicpes/tests/` y su respecto `__init__.py` + + +- Añadir app `recipe` en [settings.py](./app/app/settings.py) + + ```py + INSTALLED_APPS = [ + ... + 'recipe', + ] + ``` + +### Tests recipe API + +[`recipe/tests/test_recipe_api.py`](./app/recipe/tests/test_recipe_api.py) + +```py +... +RECIPES_URL = reverse('recipe:recipe-list') + +def create_recipe(user, **params): + """Create and return a sample recipe.""" + defaults = { + 'title': 'Titulo reseta de ejemplo', + 'time_minutes': 31, + 'price': Decimal('5.25'), + 'description': 'Descripción de ejmplo', + 'link': 'https://defzn.kickto.net/blog', + } + 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() + + 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) + + +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_retrive_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 = get_user_model().objects.create_user( + 'other@example.com', + '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) +``` + +### Serializador para Recetas + +[`recipe/serializer.py`](./app/recipe/serializers.py) + +```py +from rest_framework import serializers +from core.models import Recipe + +class RecipeSerializer(serializers.ModelSerializer): + """Serializer for recipes.""" + + class Meta: + model = Recipe + fileds = ['id', 'title', 'time_minutes', 'price', 'link'] + read_only_fields = ['id'] +``` + +### Vista Recetas + +[`recipe/views.py`](./app/recipe/views.py) + +```py +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): + """View for manage recipe APIs.""" + serializer_class = serializers.RecipeSerializer + queryset = Recipe.objects.all() + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + """Retrieve recipes for authenticated user.""" + return self.queryset.filter(user=self.request.user).order_by('-id') +``` + +### URLs Recetas + +[`recipe/urls.py`](./app/recipe/urls.py) + +```py +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)), +] +``` + +[`app/urls.py`](./app/app/urls.py) + +```py +... + +urlpatterns = [ + ... + path('api/recipe', include('recipe.urls')), +] +``` diff --git a/app/app/settings.py b/app/app/settings.py index dd06dfd..b00e4f9 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = [ 'rest_framework.authtoken', 'drf_spectacular', 'user', + 'recipe', ] MIDDLEWARE = [ diff --git a/app/app/urls.py b/app/app/urls.py index 5a8bedf..dcd19b7 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -26,4 +26,5 @@ urlpatterns = [ SpectacularSwaggerView.as_view(url_name='api-schema'), name='api-docs'), path('api/user/', include('user.urls')), + path('api/recipe', include('recipe.urls')), ] diff --git a/app/core/models.py b/app/core/models.py index 1876d75..046c020 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -54,7 +54,7 @@ class Recipe(models.Model): title = models.CharField(max_length=255) description = models.TextField(blank=True) time_minutes = models.IntegerField() - price = models.DecimalField(max_digits=5,decimal_places=2, blank=True) + price = models.DecimalField(max_digits=5, decimal_places=2, blank=True) link = models.CharField(max_length=255, blank=True) def __str__(self): diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py index cf0418b..0ac5a3b 100644 --- a/app/core/tests/test_models.py +++ b/app/core/tests/test_models.py @@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model from core import models + class ModelTests(TestCase): """Test models.""" diff --git a/app/recipe/__init__.py b/app/recipe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/recipe/apps.py b/app/recipe/apps.py new file mode 100644 index 0000000..9133199 --- /dev/null +++ b/app/recipe/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RecipeConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'recipe' diff --git a/app/recipe/serializers.py b/app/recipe/serializers.py new file mode 100644 index 0000000..b3bded3 --- /dev/null +++ b/app/recipe/serializers.py @@ -0,0 +1,15 @@ +""" +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'] diff --git a/app/recipe/tests/__init__.py b/app/recipe/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/recipe/tests/test_recipe_api.py b/app/recipe/tests/test_recipe_api.py new file mode 100644 index 0000000..67351cd --- /dev/null +++ b/app/recipe/tests/test_recipe_api.py @@ -0,0 +1,87 @@ +""" +Test for reicpe 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 + + +RECIPES_URL = reverse('recipe:recipe-list') + + +def create_recipe(user, **params): + """Create and return a sample recipe.""" + defaults = { + 'title': 'Titulo reseta de ejemplo', + 'time_minutes': 31, + 'price': Decimal('5.25'), + 'description': 'Descripción de ejmplo', + 'link': 'https://defzn.kickto.net/blog', + } + 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() + + 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) + + +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_retrive_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 = get_user_model().objects.create_user( + 'other@example.com', + '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) diff --git a/app/recipe/urls.py b/app/recipe/urls.py new file mode 100644 index 0000000..b254fa3 --- /dev/null +++ b/app/recipe/urls.py @@ -0,0 +1,18 @@ +""" +URL mappings for the 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)), +] diff --git a/app/recipe/views.py b/app/recipe/views.py new file mode 100644 index 0000000..e47e50e --- /dev/null +++ b/app/recipe/views.py @@ -0,0 +1,21 @@ +""" +Views for the recipe APIs. +""" +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): + """View for manage recipe APIs.""" + serializer_class = serializers.RecipeSerializer + queryset = Recipe.objects.all() + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + """Retrieve recipes for authenticated user.""" + return self.queryset.filter(user=self.request.user).order_by('-id')