Implementación de filtros por tag y recetas
This commit is contained in:
parent
d20158552c
commit
e22bb8f062
1052
01_user_api.md
Normal file
1052
01_user_api.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -539,8 +539,10 @@ URL `localhost:8000/api/docs/`
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
- 1ra parte -> [API Recetas](./README.md)
|
- [Inicio](./README.md)
|
||||||
- 3ra parte -> [Tags](./README3.md)
|
- [User API](./01_user_api.md)
|
||||||
- 4ta parte -> [Ingredientes](./README4.md)
|
- [**Recipe API**](./02_recipe_api.md)
|
||||||
- 5ta parte -> [Imagenes](./README5.md)
|
- [Tag API](./03_tag_api.md)
|
||||||
- 6ta parte -> [filtrado](./README6.md)
|
- [Ingredient API](./04_ingredient_api.md)
|
||||||
|
- [Image API](./05_image_api.md)
|
||||||
|
- [Filters](./06_filters.md)
|
@ -517,8 +517,10 @@ URL `localhost:8000/api/docs`
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
- 1ra parte -> [API Recetas](./README.md)
|
- [Inicio](./README.md)
|
||||||
- 2da parte -> [Recetas](./README2.md)
|
- [User API](./01_user_api.md)
|
||||||
- 4ta parte -> [Ingredientes](./README4.md)
|
- [Recipe API](./02_recipe_api.md)
|
||||||
- 5ta parte -> [Imagenes](./README5.md)
|
- [**Tag API**](./03_tag_api.md)
|
||||||
- 6ta parte -> [Filtrado](./README6.md)
|
- [Ingredient API](./04_ingredient_api.md)
|
||||||
|
- [Image API](./05_image_api.md)
|
||||||
|
- [Filters](./06_filters.md)
|
@ -509,8 +509,10 @@ herencia
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
- 1ra parte -> [API Recetas](./README.md)
|
- [Inicio](./README.md)
|
||||||
- 2da parte -> [Recetas](./README2.md)
|
- [User API](./01_user_api.md)
|
||||||
- 3ra parte -> [Tags](./README3.md)
|
- [Recipe API](./02_recipe_api.md)
|
||||||
- 5ta parte -> [Imagenes](./README5.md)
|
- [Tag API](./03_tag_api.md)
|
||||||
- 6ta parte -> [Filtrado](./README6.md)
|
- [**Ingredient API**](./04_ingredient_api.md)
|
||||||
|
- [Image API](./05_image_api.md)
|
||||||
|
- [Filters](./06_filters.md)
|
@ -440,8 +440,10 @@ Levantar aplicación `docker compose up` y visitar `locahost:8000/api/docs`
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
- 1ra parte -> [API Recetas](./README.md)
|
- [Inicio](./README.md)
|
||||||
- 2da parte -> [Recetas](./README2.md)
|
- [User API](./01_user_api.md)
|
||||||
- 3ra parte -> [Tags](./README3.md)
|
- [Recipe API](./02_recipe_api.md)
|
||||||
- 4ta parte -> [Ingredientes](./README4.md)
|
- [Tag API](./03_tag_api.md)
|
||||||
- 6ta parte -> [Filtrado](./README6.md)
|
- [Ingredient API](./04_ingredient_api.md)
|
||||||
|
- [**Image API**](./05_image_api.md)
|
||||||
|
- [Filters](./06_filters.md)
|
302
06_filters.md
Normal file
302
06_filters.md
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# Filters
|
||||||
|
|
||||||
|
## Diseño
|
||||||
|
|
||||||
|
- Filtro de recetas por ingredientes/tags
|
||||||
|
- Filtro de ingredientes/tags asignados a recetas, faciltando una lista para
|
||||||
|
elegir
|
||||||
|
- Definición de parametros **OpenAPI**, actualizar documentación
|
||||||
|
|
||||||
|
### Requests de ejemplo
|
||||||
|
|
||||||
|
- Filtrar recetas por **tags(s)**
|
||||||
|
- `GET` `/api/recipe/recipes/?tags=1,2,3`
|
||||||
|
- Filtrar recetas por **ingrediente(s)**
|
||||||
|
- `GET` `/api/recipe/recipes/?ingredients=1,2,3`
|
||||||
|
- Filtrar tags por **receta asignada**
|
||||||
|
- `GET` `/api/recipe/tags/?assigned_only=1`
|
||||||
|
- Filtrar ingredientes por **receta asignada**
|
||||||
|
- `GET` `/api/recipe/ingredients/?assigned_only=1`
|
||||||
|
|
||||||
|
### OpenAPI Schema
|
||||||
|
|
||||||
|
- *Schema* auto generada
|
||||||
|
- Configuracion manual de cierta documentación (custom query parmas filtering)
|
||||||
|
- Uso del decorador `extend_schema_view` de **DRF_Spectacular**. Ej.
|
||||||
|
|
||||||
|
```py
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
'tags',
|
||||||
|
OpenApiTypes.STR,
|
||||||
|
description='Coma separated list of tags IDs to filter',
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
'ingredients',
|
||||||
|
OpenApiTypes.STR,
|
||||||
|
description='Coma separated list of ingredients IDs to filter',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class RecipeViewSet(viewsets.ModelViewSet):
|
||||||
|
"""View for manage recipe APIs."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test filtros
|
||||||
|
|
||||||
|
[`test_recipe_api.py`](./app/recipe/tests/test_recipe_api.py)
|
||||||
|
|
||||||
|
```py
|
||||||
|
class PrivateRecipeApiTests(TestCase):
|
||||||
|
"""Test authenticated API requests."""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
def test_fitler_by_tags(self):
|
||||||
|
"""Test filtering recipes by tags."""
|
||||||
|
r1 = create_recipe(user=self.user, title='Sopa de Verduras')
|
||||||
|
r2 = create_recipe(user=self.user, title='Arroz con Huevo')
|
||||||
|
tag1 = Tag.objects.create(user=self.user, name='Vergan')
|
||||||
|
tag2 = Tag.objects.create(user=self.user, name='Vegetariana')
|
||||||
|
r1.tags.add(tag1)
|
||||||
|
r2.tags.add(tag2)
|
||||||
|
r3 = create_recipe(user=self.user, title='Pure con Prietas')
|
||||||
|
|
||||||
|
params = {'tags': f'{tag1.id}, {tag2.id}'}
|
||||||
|
res = self.client.get(RECIPES_URL, params)
|
||||||
|
|
||||||
|
s1 = RecipeSerializer(r1)
|
||||||
|
s2 = RecipeSerializer(r2)
|
||||||
|
s3 = RecipeSerializer(r3)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertIn(s2.data, res.data)
|
||||||
|
self.assertNotIn(s3.data, res.data)
|
||||||
|
|
||||||
|
def test_filter_by_ingredients(self):
|
||||||
|
"""Test filtering recipes by ingredients."""
|
||||||
|
r1 = create_recipe(user=self.user, title='Porotos con rienda')
|
||||||
|
r2 = create_recipe(user=self.user, title='Pollo al jugo')
|
||||||
|
in1 = Ingredient.objects.create(user=self.user, name='Porotos')
|
||||||
|
in2 = Ingredient.objects.create(user=self.user, name='Pollo')
|
||||||
|
r1.ingredients.add(in1)
|
||||||
|
r2.ingredients.add(in2)
|
||||||
|
r3 = create_recipe(user=self.user, title='Lentejas con arroz')
|
||||||
|
|
||||||
|
params = {'ingredients': f'{in1.id}, {in2.id}'}
|
||||||
|
res = self.client.get(RECIPES_URL, params)
|
||||||
|
|
||||||
|
s1 = RecipeSerializer(r1)
|
||||||
|
s2 = RecipeSerializer(r2)
|
||||||
|
s3 = RecipeSerializer(r3)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertIn(s2.data, res.data)
|
||||||
|
self.assertNotIn(s3.data, res.data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementación filtros
|
||||||
|
|
||||||
|
[`recipe/views.py`](./app/recipe/views.py)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import (
|
||||||
|
extend_schema_view,
|
||||||
|
extend_schema,
|
||||||
|
OpenApiParameter,
|
||||||
|
)
|
||||||
|
...
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
'tags',
|
||||||
|
OpenApiTypes.STR,
|
||||||
|
description='Lista separada por coma de tags IDs a filtrar'
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
'ingredients',
|
||||||
|
OpenApiTypes.STR,
|
||||||
|
description='Lista separada por coma de ingredientes IDs a \
|
||||||
|
filtrar'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class RecipeViewSet(viewsets.ModelViewSet):
|
||||||
|
...
|
||||||
|
|
||||||
|
def _params_to_ints(self, qs):
|
||||||
|
"""Convert a list of strings to integers."""
|
||||||
|
return [int(str_id) for str_id in qs.split(',')]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Retrieve recipes for authenticated user."""
|
||||||
|
tags = self.request.query_params.get('tags')
|
||||||
|
ingredients = self.request.query_params.get('ingredients')
|
||||||
|
queryset = self.queryset
|
||||||
|
if tags:
|
||||||
|
tag_ids = self._params_to_ints(tags)
|
||||||
|
queryset = queryset.filter(tags__id__in=tag_ids)
|
||||||
|
if ingredients:
|
||||||
|
ingredients_ids = self._params_to_ints(ingredients)
|
||||||
|
queryset = queryset.filter(ingredients__id__in=ingredients_ids)
|
||||||
|
|
||||||
|
return queryset.filter(
|
||||||
|
user=self.request.user
|
||||||
|
).order_by('-id').distinct()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test para filtrar por tags e ingredientes
|
||||||
|
|
||||||
|
[`test_ingredients_api.py`](./app/recipe/tests/test_ingredients_api.py)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from decimal import Decimal
|
||||||
|
from core.model import Recipe
|
||||||
|
...
|
||||||
|
|
||||||
|
def test_filter_ingredients_assigned_to_recipes(self):
|
||||||
|
"""Test listing ingredients by those assigned to recipes."""
|
||||||
|
in1 = Ingredient.objects.create(user=self.user, name='Manzana')
|
||||||
|
in2 = Ingredient.objects.create(user=self.user, name='Pavo')
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
title='Pure de Manzana',
|
||||||
|
time_minutes=5,
|
||||||
|
price=Decimal('4.5'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe.ingredients.add(in1)
|
||||||
|
|
||||||
|
res = self.client.get(INGREDIENTS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
s1 = IngredientSerializer(in1)
|
||||||
|
s2 = IngredientSerializer(in2)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertNotIn(s2.data, res.data)
|
||||||
|
|
||||||
|
def test_filtered_ingredients_unique(self):
|
||||||
|
"""Test filtered ingredients returns a unique list."""
|
||||||
|
ing = Ingredient.objects.create(user=self.user, name='Huevo')
|
||||||
|
Ingredient.objects.create(user=self.user, name='Lentejas')
|
||||||
|
recipe1 = Recipe.objects.create(
|
||||||
|
title='Huevos a la copa',
|
||||||
|
time_minutes=4,
|
||||||
|
price=Decimal('1.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe2 = Recipe.objects.create(
|
||||||
|
title='Huevos a cocidos',
|
||||||
|
time_minutes=5,
|
||||||
|
price=Decimal('1.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe1.ingredients.add(ing)
|
||||||
|
recipe2.ingredients.add(ing)
|
||||||
|
|
||||||
|
res = self.client.get(INGREDIENTS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
self.assertEqual(len(res.data), 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
[`test_tags_api.py`](./app/recipe/tests/test_tags_api.py)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from decimal import Decimal
|
||||||
|
from core.model import Recipe
|
||||||
|
...
|
||||||
|
|
||||||
|
def test_filter_tags_assigned_to_recipes(self):
|
||||||
|
"""Test listing tags to those assigned to recipes."""
|
||||||
|
tag1 = Tag.objects.create(user=self.user, name='Desayuno')
|
||||||
|
tag2 = Tag.objects.create(user=self.user, name='Almuerzo')
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
title='Huevos Fritos',
|
||||||
|
time_minutes='5',
|
||||||
|
price=Decimal('2.5'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe.tags.add(tag1)
|
||||||
|
|
||||||
|
res = self.client.get(TAGS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
s1 = TagSerializer(tag1)
|
||||||
|
s2 = TagSerializer(tag2)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertNotIn(s2.data, res.data)
|
||||||
|
|
||||||
|
def test_filter_tags_unique(self):
|
||||||
|
"""Test filtered tags retunrs a unique list."""
|
||||||
|
tag = Tag.objects.create(user=self.user, name='Desayuno')
|
||||||
|
Tag.objects.create(user=self.user, name='Almuerzo')
|
||||||
|
recipe1 = Recipe.objects.create(
|
||||||
|
title='Panqueques',
|
||||||
|
time_minutes='25',
|
||||||
|
price=Decimal('5.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe2 = Recipe.objects.create(
|
||||||
|
title='Avena con fruta',
|
||||||
|
time_minutes='15',
|
||||||
|
price=Decimal('7.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe1.tags.add(tag)
|
||||||
|
recipe2.tags.add(tag)
|
||||||
|
|
||||||
|
res = self.client.get(TAGS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
self.assertEqual(len(res.data), 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementación filtrado por tags e ingredientes
|
||||||
|
|
||||||
|
[`recipe/views.py`](./app/recipe/views.py)
|
||||||
|
|
||||||
|
```py
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
'assigned_only',
|
||||||
|
OpenApiTypes.INT, enum=[0, 1],
|
||||||
|
description='Filtro por items asignados a recetas.'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class BaseRecipeAtrrViewSet(mixins.DestroyModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
viewsets.GenericViewSet):
|
||||||
|
"""Base viewset for recipe attributes."""
|
||||||
|
authentication_classes = [TokenAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Filter queryset to authenticated user."""
|
||||||
|
assigned_only = bool(
|
||||||
|
int(self.request.query_params.get('assigned_only', 0))
|
||||||
|
)
|
||||||
|
queryset = self.queryset
|
||||||
|
if assigned_only:
|
||||||
|
queryset = queryset.filter(recipe__isnull=False)
|
||||||
|
|
||||||
|
return queryset.filter(
|
||||||
|
user=self.request.user
|
||||||
|
).order_by('-name').distinct()
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
- [**Inicio**](./README.md)
|
||||||
|
- [User API](./01_user_api.md)
|
||||||
|
- [Recipe API](./02_recipe_api.md)
|
||||||
|
- [Tag API](./03_tag_api.md)
|
||||||
|
- [Ingredient API](./04_ingredient_api.md)
|
||||||
|
- [Image API](./05_image_api.md)
|
||||||
|
- [Filters](./06_filters.md)
|
@ -1,6 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Tests for the ingredients API.
|
Tests for the ingredients API.
|
||||||
"""
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@ -8,7 +10,10 @@ from django.test import TestCase
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core.models import Ingredient
|
from core.models import (
|
||||||
|
Ingredient,
|
||||||
|
Recipe,
|
||||||
|
)
|
||||||
|
|
||||||
from recipe.serializers import IngredientSerializer
|
from recipe.serializers import IngredientSerializer
|
||||||
|
|
||||||
@ -97,3 +102,45 @@ class PrivateIngredientsApiTests(TestCase):
|
|||||||
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
ingredients = Ingredient.objects.filter(user=self.user)
|
ingredients = Ingredient.objects.filter(user=self.user)
|
||||||
self.assertFalse(ingredients.exists())
|
self.assertFalse(ingredients.exists())
|
||||||
|
|
||||||
|
def test_filter_ingredients_assigned_to_recipes(self):
|
||||||
|
"""Test listing ingredients by those assigned to recipes."""
|
||||||
|
in1 = Ingredient.objects.create(user=self.user, name='Manzana')
|
||||||
|
in2 = Ingredient.objects.create(user=self.user, name='Pavo')
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
title='Pure de Manzana',
|
||||||
|
time_minutes=5,
|
||||||
|
price=Decimal('4.5'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe.ingredients.add(in1)
|
||||||
|
|
||||||
|
res = self.client.get(INGREDIENTS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
s1 = IngredientSerializer(in1)
|
||||||
|
s2 = IngredientSerializer(in2)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertNotIn(s2.data, res.data)
|
||||||
|
|
||||||
|
def test_filtered_ingredients_unique(self):
|
||||||
|
"""Test filtered ingredients returns a unique list."""
|
||||||
|
ing = Ingredient.objects.create(user=self.user, name='Huevo')
|
||||||
|
Ingredient.objects.create(user=self.user, name='Lentejas')
|
||||||
|
recipe1 = Recipe.objects.create(
|
||||||
|
title='Huevos a la copa',
|
||||||
|
time_minutes=4,
|
||||||
|
price=Decimal('1.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe2 = Recipe.objects.create(
|
||||||
|
title='Huevos a cocidos',
|
||||||
|
time_minutes=5,
|
||||||
|
price=Decimal('1.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe1.ingredients.add(ing)
|
||||||
|
recipe2.ingredients.add(ing)
|
||||||
|
|
||||||
|
res = self.client.get(INGREDIENTS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
self.assertEqual(len(res.data), 1)
|
||||||
|
@ -394,6 +394,46 @@ class PrivateRecipeApiTests(TestCase):
|
|||||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(recipe.ingredients.count(), 0)
|
self.assertEqual(recipe.ingredients.count(), 0)
|
||||||
|
|
||||||
|
def test_filter_by_tags(self):
|
||||||
|
"""Test filtering recipes by tags."""
|
||||||
|
r1 = create_recipe(user=self.user, title='Sopa de Verduras')
|
||||||
|
r2 = create_recipe(user=self.user, title='Arroz con Huevo')
|
||||||
|
tag1 = Tag.objects.create(user=self.user, name='Vergan')
|
||||||
|
tag2 = Tag.objects.create(user=self.user, name='Vegetariana')
|
||||||
|
r1.tags.add(tag1)
|
||||||
|
r2.tags.add(tag2)
|
||||||
|
r3 = create_recipe(user=self.user, title='Pure con Prietas')
|
||||||
|
|
||||||
|
params = {'tags': f'{tag1.id}, {tag2.id}'}
|
||||||
|
res = self.client.get(RECIPES_URL, params)
|
||||||
|
|
||||||
|
s1 = RecipeSerializer(r1)
|
||||||
|
s2 = RecipeSerializer(r2)
|
||||||
|
s3 = RecipeSerializer(r3)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertIn(s2.data, res.data)
|
||||||
|
self.assertNotIn(s3.data, res.data)
|
||||||
|
|
||||||
|
def test_filter_by_ingredients(self):
|
||||||
|
"""Test filtering recipes by ingredients."""
|
||||||
|
r1 = create_recipe(user=self.user, title='Porotos con rienda')
|
||||||
|
r2 = create_recipe(user=self.user, title='Pollo al jugo')
|
||||||
|
in1 = Ingredient.objects.create(user=self.user, name='Porotos')
|
||||||
|
in2 = Ingredient.objects.create(user=self.user, name='Pollo')
|
||||||
|
r1.ingredients.add(in1)
|
||||||
|
r2.ingredients.add(in2)
|
||||||
|
r3 = create_recipe(user=self.user, title='Lentejas con arroz')
|
||||||
|
|
||||||
|
params = {'ingredients': f'{in1.id}, {in2.id}'}
|
||||||
|
res = self.client.get(RECIPES_URL, params)
|
||||||
|
|
||||||
|
s1 = RecipeSerializer(r1)
|
||||||
|
s2 = RecipeSerializer(r2)
|
||||||
|
s3 = RecipeSerializer(r3)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertIn(s2.data, res.data)
|
||||||
|
self.assertNotIn(s3.data, res.data)
|
||||||
|
|
||||||
|
|
||||||
class ImageUploadTest(TestCase):
|
class ImageUploadTest(TestCase):
|
||||||
"""Tests for the image upload API."""
|
"""Tests for the image upload API."""
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Tests for tags API
|
Tests for tags API
|
||||||
"""
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@ -8,7 +10,10 @@ from django.test import TestCase
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core.models import Tag
|
from core.models import (
|
||||||
|
Tag,
|
||||||
|
Recipe,
|
||||||
|
)
|
||||||
|
|
||||||
from recipe.serializers import TagSerializer
|
from recipe.serializers import TagSerializer
|
||||||
|
|
||||||
@ -94,3 +99,45 @@ class PrivateTagsApiTests(TestCase):
|
|||||||
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
tags = Tag.objects.filter(user=self.user)
|
tags = Tag.objects.filter(user=self.user)
|
||||||
self.assertFalse(tags.exists())
|
self.assertFalse(tags.exists())
|
||||||
|
|
||||||
|
def test_filter_tags_assigned_to_recipes(self):
|
||||||
|
"""Test listing tags to those assigned to recipes."""
|
||||||
|
tag1 = Tag.objects.create(user=self.user, name='Desayuno')
|
||||||
|
tag2 = Tag.objects.create(user=self.user, name='Almuerzo')
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
title='Huevos Fritos',
|
||||||
|
time_minutes='5',
|
||||||
|
price=Decimal('2.5'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe.tags.add(tag1)
|
||||||
|
|
||||||
|
res = self.client.get(TAGS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
s1 = TagSerializer(tag1)
|
||||||
|
s2 = TagSerializer(tag2)
|
||||||
|
self.assertIn(s1.data, res.data)
|
||||||
|
self.assertNotIn(s2.data, res.data)
|
||||||
|
|
||||||
|
def test_filter_tags_unique(self):
|
||||||
|
"""Test filtered tags retunrs a unique list."""
|
||||||
|
tag = Tag.objects.create(user=self.user, name='Desayuno')
|
||||||
|
Tag.objects.create(user=self.user, name='Almuerzo')
|
||||||
|
recipe1 = Recipe.objects.create(
|
||||||
|
title='Panqueques',
|
||||||
|
time_minutes='25',
|
||||||
|
price=Decimal('5.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe2 = Recipe.objects.create(
|
||||||
|
title='Avena con fruta',
|
||||||
|
time_minutes='15',
|
||||||
|
price=Decimal('7.0'),
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
recipe1.tags.add(tag)
|
||||||
|
recipe2.tags.add(tag)
|
||||||
|
|
||||||
|
res = self.client.get(TAGS_URL, {'assigned_only': 1})
|
||||||
|
|
||||||
|
self.assertEqual(len(res.data), 1)
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Views for the recipe APIs.
|
Views for the recipe APIs.
|
||||||
"""
|
"""
|
||||||
|
from drf_spectacular.utils import (
|
||||||
|
extend_schema_view,
|
||||||
|
extend_schema,
|
||||||
|
OpenApiParameter,
|
||||||
|
)
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework import (
|
from rest_framework import (
|
||||||
viewsets,
|
viewsets,
|
||||||
mixins,
|
mixins,
|
||||||
@ -19,6 +25,23 @@ from core.models import (
|
|||||||
from recipe import serializers
|
from recipe import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
'tags',
|
||||||
|
OpenApiTypes.STR,
|
||||||
|
description='Lista separada por coma de tags IDs a filtrar'
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
'ingredients',
|
||||||
|
OpenApiTypes.STR,
|
||||||
|
description='Lista separada por coma de ingredientes IDs a \
|
||||||
|
filtrar'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
class RecipeViewSet(viewsets.ModelViewSet):
|
class RecipeViewSet(viewsets.ModelViewSet):
|
||||||
"""View for manage recipe APIs."""
|
"""View for manage recipe APIs."""
|
||||||
serializer_class = serializers.RecipeDetailSerializer
|
serializer_class = serializers.RecipeDetailSerializer
|
||||||
@ -26,9 +49,25 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
authentication_classes = [TokenAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def _params_to_ints(self, qs):
|
||||||
|
"""Convert a list of strings to integers."""
|
||||||
|
return [int(str_id) for str_id in qs.split(',')]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Retrieve recipes for authenticated user."""
|
"""Retrieve recipes for authenticated user."""
|
||||||
return self.queryset.filter(user=self.request.user).order_by('-id')
|
tags = self.request.query_params.get('tags')
|
||||||
|
ingredients = self.request.query_params.get('ingredients')
|
||||||
|
queryset = self.queryset
|
||||||
|
if tags:
|
||||||
|
tag_ids = self._params_to_ints(tags)
|
||||||
|
queryset = queryset.filter(tags__id__in=tag_ids)
|
||||||
|
if ingredients:
|
||||||
|
ingredients_ids = self._params_to_ints(ingredients)
|
||||||
|
queryset = queryset.filter(ingredients__id__in=ingredients_ids)
|
||||||
|
|
||||||
|
return queryset.filter(
|
||||||
|
user=self.request.user
|
||||||
|
).order_by('-id').distinct()
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
"""Return the serializer class for request."""
|
"""Return the serializer class for request."""
|
||||||
@ -56,6 +95,17 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
'assigned_only',
|
||||||
|
OpenApiTypes.INT, enum=[0, 1],
|
||||||
|
description='Filtro por items asignados a recetas.'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
class BaseRecipeAtrrViewSet(mixins.DestroyModelMixin,
|
class BaseRecipeAtrrViewSet(mixins.DestroyModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
@ -66,7 +116,16 @@ class BaseRecipeAtrrViewSet(mixins.DestroyModelMixin,
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Filter queryset to authenticated user."""
|
"""Filter queryset to authenticated user."""
|
||||||
return self.queryset.filter(user=self.request.user).order_by('-name')
|
assigned_only = bool(
|
||||||
|
int(self.request.query_params.get('assigned_only', 0))
|
||||||
|
)
|
||||||
|
queryset = self.queryset
|
||||||
|
if assigned_only:
|
||||||
|
queryset = queryset.filter(recipe__isnull=False)
|
||||||
|
|
||||||
|
return queryset.filter(
|
||||||
|
user=self.request.user
|
||||||
|
).order_by('-name').distinct()
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(BaseRecipeAtrrViewSet):
|
class TagViewSet(BaseRecipeAtrrViewSet):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Django==4.2.5
|
Django==4.2.5
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
psycopg2>=2.9.9
|
psycopg2==2.9.9
|
||||||
drf-spectacular>=0.16
|
drf-spectacular==0.26.5
|
||||||
Pillow>=10
|
Pillow==10.0.1
|
||||||
|
Loading…
Reference in New Issue
Block a user