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.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.
|
||||
"""
|
||||
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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user