Recipe API, tests, model & create
This commit is contained in:
parent
f8ac495c2d
commit
f4c53c0718
117
README2.md
Normal file
117
README2.md
Normal 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
|
||||||
|
```
|
@ -44,3 +44,4 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
|
|
||||||
|
|
||||||
admin.site.register(models.User, UserAdmin)
|
admin.site.register(models.User, UserAdmin)
|
||||||
|
admin.site.register(models.Recipe)
|
||||||
|
27
app/core/migrations/0002_recipe.py
Normal file
27
app/core/migrations/0002_recipe.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Databse models.
|
Databse models.
|
||||||
"""
|
"""
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
AbstractBaseUser,
|
AbstractBaseUser,
|
||||||
@ -42,3 +43,19 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
USERNAME_FIELD = 'email'
|
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
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Test for models.
|
Test for models.
|
||||||
"""
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from core import models
|
||||||
|
|
||||||
class ModelTests(TestCase):
|
class ModelTests(TestCase):
|
||||||
"""Test models."""
|
"""Test models."""
|
||||||
@ -44,3 +47,19 @@ class ModelTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(user.is_superuser)
|
self.assertTrue(user.is_superuser)
|
||||||
self.assertTrue(user.is_staff)
|
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)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Seralizers for the user API View
|
Seralizers for the user API View
|
||||||
"""
|
"""
|
||||||
from django.contrib.auth import get_user_model, authenticate
|
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
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ CREATE_USER_URL = reverse('user:create')
|
|||||||
TOKEN_URL = reverse('user:token')
|
TOKEN_URL = reverse('user:token')
|
||||||
ME_URL = reverse('user:me')
|
ME_URL = reverse('user:me')
|
||||||
|
|
||||||
|
|
||||||
def create_user(**params):
|
def create_user(**params):
|
||||||
"""Create and return a new user."""
|
"""Create and return a new user."""
|
||||||
return get_user_model().objects.create_user(**params)
|
return get_user_model().objects.create_user(**params)
|
||||||
@ -27,7 +28,7 @@ class PublicUserApiTest(TestCase):
|
|||||||
"""Tests creating a user is successful."""
|
"""Tests creating a user is successful."""
|
||||||
payload = {
|
payload = {
|
||||||
'email': 'test@example.com',
|
'email': 'test@example.com',
|
||||||
'password':'testpass123',
|
'password': 'testpass123',
|
||||||
'name': 'TestName',
|
'name': 'TestName',
|
||||||
}
|
}
|
||||||
res = self.client.post(CREATE_USER_URL, payload)
|
res = self.client.post(CREATE_USER_URL, payload)
|
||||||
@ -41,7 +42,7 @@ class PublicUserApiTest(TestCase):
|
|||||||
"""Test error returned if user with email exists."""
|
"""Test error returned if user with email exists."""
|
||||||
payload = {
|
payload = {
|
||||||
'email': 'test@example.com',
|
'email': 'test@example.com',
|
||||||
'password':'testpass123',
|
'password': 'testpass123',
|
||||||
'name': 'Test Name',
|
'name': 'Test Name',
|
||||||
}
|
}
|
||||||
create_user(**payload)
|
create_user(**payload)
|
||||||
@ -53,7 +54,7 @@ class PublicUserApiTest(TestCase):
|
|||||||
"""Test an error is returned if password less than 5 chars."""
|
"""Test an error is returned if password less than 5 chars."""
|
||||||
payload = {
|
payload = {
|
||||||
'email': 'test@example.com',
|
'email': 'test@example.com',
|
||||||
'password':'pw',
|
'password': 'pw',
|
||||||
'name': 'Test Name',
|
'name': 'Test Name',
|
||||||
}
|
}
|
||||||
res = self.client.post(CREATE_USER_URL, payload)
|
res = self.client.post(CREATE_USER_URL, payload)
|
||||||
@ -69,7 +70,7 @@ class PublicUserApiTest(TestCase):
|
|||||||
user_details = {
|
user_details = {
|
||||||
'name': 'Test Name',
|
'name': 'Test Name',
|
||||||
'email': 'test@example.com',
|
'email': 'test@example.com',
|
||||||
'password':'test-user-password123',
|
'password': 'test-user-password123',
|
||||||
}
|
}
|
||||||
create_user(**user_details)
|
create_user(**user_details)
|
||||||
|
|
||||||
@ -86,15 +87,15 @@ class PublicUserApiTest(TestCase):
|
|||||||
"""Test returns error if credentials invalid."""
|
"""Test returns error if credentials invalid."""
|
||||||
create_user(email='test@example.com', password='goodpass')
|
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)
|
res = self.client.post(TOKEN_URL, payload)
|
||||||
|
|
||||||
self.assertNotIn('token', res.data)
|
self.assertNotIn('token', res.data)
|
||||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def test_create_token_blank_password(self):
|
def test_create_token_blank_password(self):
|
||||||
"""Test posting a blank password returns an error."""
|
"""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)
|
res = self.client.post(TOKEN_URL, payload)
|
||||||
|
|
||||||
self.assertNotIn('token', res.data)
|
self.assertNotIn('token', res.data)
|
||||||
@ -103,7 +104,7 @@ class PublicUserApiTest(TestCase):
|
|||||||
def test_retrive_user_unauthorized(self):
|
def test_retrive_user_unauthorized(self):
|
||||||
"""Test authentication is required for users."""
|
"""Test authentication is required for users."""
|
||||||
res = self.client.get(ME_URL)
|
res = self.client.get(ME_URL)
|
||||||
|
|
||||||
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
|
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +129,8 @@ class PrivateUserApiTests(TestCase):
|
|||||||
res.data, {
|
res.data, {
|
||||||
'name': self.user.name,
|
'name': self.user.name,
|
||||||
'email': self.user.email,
|
'email': self.user.email,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_post_me_not_allowed(self):
|
def test_post_me_not_allowed(self):
|
||||||
"""Test POST is not allowed for the 'me' endpoint."""
|
"""Test POST is not allowed for the 'me' endpoint."""
|
||||||
@ -141,8 +143,8 @@ class PrivateUserApiTests(TestCase):
|
|||||||
|
|
||||||
def test_update_user_profile(self):
|
def test_update_user_profile(self):
|
||||||
"""Test updating the user profile for the autenticated user."""
|
"""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)
|
res = self.client.patch(ME_URL, payload)
|
||||||
|
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
|
@ -6,7 +6,7 @@ from django.urls import path
|
|||||||
from user import views
|
from user import views
|
||||||
|
|
||||||
|
|
||||||
app_name='user'
|
app_name = 'user'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('create/', views.CreateUserView.as_view(), name='create'),
|
path('create/', views.CreateUserView.as_view(), name='create'),
|
||||||
|
@ -18,6 +18,7 @@ class CreateTokenView(ObtainAuthToken):
|
|||||||
serializer_class = AuthTokenSerializer
|
serializer_class = AuthTokenSerializer
|
||||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||||
|
|
||||||
|
|
||||||
class ManageUserView(generics.RetrieveUpdateAPIView):
|
class ManageUserView(generics.RetrieveUpdateAPIView):
|
||||||
"""Manage the autenticated user."""
|
"""Manage the autenticated user."""
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
Loading…
Reference in New Issue
Block a user