diff --git a/README2.md b/README2.md new file mode 100644 index 0000000..4f1e177 --- /dev/null +++ b/README2.md @@ -0,0 +1,117 @@ +# Recipe API + +#### Caracteristicas + +- Crear +- Listar +- Ver detalles +- Actualizar +- Borrar + +#### Endpoints + +- `/recipes/` + - `GET` Listar todas las recetas + - `POST` Crea recetas +- `/recipes/`/ + - `GET` Ver detalles de receta + - `PUT/PATCH` Actualizar receta + - `DELETE` Borrar receta + +### APIView vs Viewsets + +Una vista maneja un request a una URL, DRF usa clases con lógica reutilizable. +DRF además soporta decoradores. + +`APIView` y `Viewsets` son clases base falicitadas por DRF. + +### APIView + +- Concentradas alrededor de los metodos HTTP +- Métodos de clase para los métodos HTTP `GET`, `POST`, `PUT`, `PATCH`, `DELETE` +- Ofrece flexibilidad sobre toda la estuctura de las URL y la lógica usada para +procesar estas peticiones +- Util para APIs sin CRUD. Lógica a la medida, ej. auth, jobs, apis externas + +### Viewsets + +- Concentradas alrededor de aciones + - Retrive, list, update, partial update, destroy +- Mapea los modelos de Django +- Usa rutas para generar URLs +- Genial para operaciones CRUD en los modelos + +## Test Create Recipe + +[core/tests/test_models.py](./app/core/tests/test_models.py) + +```py +from decimal import Decimal + +... + +from core import models + +class ModelTests(TestCase): + ... + + def test_create_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='Nombre receta ejemplo', + time_minutes=5, + price=Decimal('5.50'), + decription='Descripción de la receta de ejemplo' + ) + + self.assertEqual(str(recipe), recipe.title) +``` + +## Creación del modelo + +[`core/models.py`](./app/core/models.py) + +```py +... + +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, blank=True) + link = models.CharField(max_length=255, blank=True) + + def __str__(self): + return self.title +``` + +### Agregar al panel de administración + +[`core/admin.py`](./app/core/admin.py) + +```py +... +admin.site.register(models.Recipe) +``` + +### Crear migraciones + +`docker compose run --rm app sh -c "python manage.py makemigrations"` + +```sh +[+] Creating 1/0 + ✔ Container recipes_api_django-db-1 Running 0.0s +Migrations for 'core': + core/migrations/0002_recipe.py + - Create model Recipe +``` diff --git a/app/core/admin.py b/app/core/admin.py index 2729c07..e5b9bfc 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -44,3 +44,4 @@ class UserAdmin(BaseUserAdmin): admin.site.register(models.User, UserAdmin) +admin.site.register(models.Recipe) diff --git a/app/core/migrations/0002_recipe.py b/app/core/migrations/0002_recipe.py new file mode 100644 index 0000000..39b28a0 --- /dev/null +++ b/app/core/migrations/0002_recipe.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.5 on 2023-10-09 04:21 + +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(blank=True, 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)), + ], + ), + ] diff --git a/app/core/models.py b/app/core/models.py index c814c00..1876d75 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -1,6 +1,7 @@ """ Databse models. """ +from django.conf import settings from django.db import models from django.contrib.auth.models import ( AbstractBaseUser, @@ -42,3 +43,19 @@ class User(AbstractBaseUser, PermissionsMixin): objects = UserManager() 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, blank=True) + link = models.CharField(max_length=255, blank=True) + + def __str__(self): + return self.title diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py index acd5051..cf0418b 100644 --- a/app/core/tests/test_models.py +++ b/app/core/tests/test_models.py @@ -1,9 +1,12 @@ """ Test for models. """ +from decimal import Decimal + from django.test import TestCase from django.contrib.auth import get_user_model +from core import models class ModelTests(TestCase): """Test models.""" @@ -44,3 +47,19 @@ class ModelTests(TestCase): ) self.assertTrue(user.is_superuser) self.assertTrue(user.is_staff) + + def test_create_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='Nombre receta ejemplo', + time_minutes=5, + price=Decimal('5.50'), + description='Descripción de la receta de ejemplo' + ) + + self.assertEqual(str(recipe), recipe.title) diff --git a/app/user/serializers.py b/app/user/serializers.py index 4dc6652..1784b4b 100644 --- a/app/user/serializers.py +++ b/app/user/serializers.py @@ -2,7 +2,7 @@ Seralizers for the user API View """ from django.contrib.auth import get_user_model, authenticate -from django.utils.translation import gettext as _, trim_whitespace +from django.utils.translation import gettext as _ # , trim_whitespace from rest_framework import serializers diff --git a/app/user/tests/test_user_api.py b/app/user/tests/test_user_api.py index 9df8ec1..6d4eeda 100644 --- a/app/user/tests/test_user_api.py +++ b/app/user/tests/test_user_api.py @@ -12,6 +12,7 @@ CREATE_USER_URL = reverse('user:create') TOKEN_URL = reverse('user:token') ME_URL = reverse('user:me') + def create_user(**params): """Create and return a new user.""" return get_user_model().objects.create_user(**params) @@ -27,7 +28,7 @@ class PublicUserApiTest(TestCase): """Tests creating a user is successful.""" payload = { 'email': 'test@example.com', - 'password':'testpass123', + 'password': 'testpass123', 'name': 'TestName', } res = self.client.post(CREATE_USER_URL, payload) @@ -41,7 +42,7 @@ class PublicUserApiTest(TestCase): """Test error returned if user with email exists.""" payload = { 'email': 'test@example.com', - 'password':'testpass123', + 'password': 'testpass123', 'name': 'Test Name', } create_user(**payload) @@ -53,7 +54,7 @@ class PublicUserApiTest(TestCase): """Test an error is returned if password less than 5 chars.""" payload = { 'email': 'test@example.com', - 'password':'pw', + 'password': 'pw', 'name': 'Test Name', } res = self.client.post(CREATE_USER_URL, payload) @@ -69,7 +70,7 @@ class PublicUserApiTest(TestCase): user_details = { 'name': 'Test Name', 'email': 'test@example.com', - 'password':'test-user-password123', + 'password': 'test-user-password123', } create_user(**user_details) @@ -86,15 +87,15 @@ class PublicUserApiTest(TestCase): """Test returns error if credentials invalid.""" create_user(email='test@example.com', password='goodpass') - payload = {'email': 'test@example.com' ,'password': 'badpass'} + payload = {'email': 'test@example.com', 'password': 'badpass'} res = self.client.post(TOKEN_URL, payload) self.assertNotIn('token', res.data) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - + def test_create_token_blank_password(self): """Test posting a blank password returns an error.""" - payload = {'email': 'test@example.com' , 'password': ''} + payload = {'email': 'test@example.com', 'password': ''} res = self.client.post(TOKEN_URL, payload) self.assertNotIn('token', res.data) @@ -103,7 +104,7 @@ class PublicUserApiTest(TestCase): def test_retrive_user_unauthorized(self): """Test authentication is required for users.""" res = self.client.get(ME_URL) - + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) @@ -128,7 +129,8 @@ class PrivateUserApiTests(TestCase): res.data, { 'name': self.user.name, 'email': self.user.email, - }) + } + ) def test_post_me_not_allowed(self): """Test POST is not allowed for the 'me' endpoint.""" @@ -141,8 +143,8 @@ class PrivateUserApiTests(TestCase): def test_update_user_profile(self): """Test updating the user profile for the autenticated user.""" - payload = { 'name': 'Updated Name', 'password': 'newpassword123' } - + payload = {'name': 'Updated Name', 'password': 'newpassword123'} + res = self.client.patch(ME_URL, payload) self.user.refresh_from_db() diff --git a/app/user/urls.py b/app/user/urls.py index 494dd50..857c790 100644 --- a/app/user/urls.py +++ b/app/user/urls.py @@ -6,7 +6,7 @@ from django.urls import path from user import views -app_name='user' +app_name = 'user' urlpatterns = [ path('create/', views.CreateUserView.as_view(), name='create'), diff --git a/app/user/views.py b/app/user/views.py index 49c7a22..69a1ffe 100644 --- a/app/user/views.py +++ b/app/user/views.py @@ -18,6 +18,7 @@ class CreateTokenView(ObtainAuthToken): serializer_class = AuthTokenSerializer renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + class ManageUserView(generics.RetrieveUpdateAPIView): """Manage the autenticated user.""" serializer_class = UserSerializer