recipes_api/06_filters.md

9.2 KiB

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.

    @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

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

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

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

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

@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()