Recipe API, tests, model & create

This commit is contained in:
devfzn 2023-10-09 01:25:27 -03:00
parent f8ac495c2d
commit f4c53c0718
Signed by: devfzn
GPG Key ID: E070ECF4A754FDB1
9 changed files with 197 additions and 13 deletions

117
README2.md Normal file
View File

@ -0,0 +1,117 @@
# Recipe API
#### Caracteristicas
- Crear
- Listar
- Ver detalles
- Actualizar
- Borrar
#### Endpoints
- `/recipes/`
- `GET` Listar todas las recetas
- `POST` Crea recetas
- `/recipes/<recipe_id>`/
- `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
```

View File

@ -44,3 +44,4 @@ class UserAdmin(BaseUserAdmin):
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Recipe)

View File

@ -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)),
],
),
]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

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

View File

@ -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'),

View File

@ -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