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