recipes_api/06_filters.md

303 lines
9.2 KiB
Markdown
Raw Normal View History

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