Search code examples
pythondjangodjango-rest-frameworkdjango-filter

Django rest filter by serializermethodfield with custom filter


As declared in question title, i got task to filter results by field not presented in model but calculated by serializer.

The model:

class Recipe(models.Model):
    tags = models.ManyToManyField(
        Tag,
        related_name='recipe_tags'
    )
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='author_recipes'
    )
    ingredients = models.ManyToManyField(
        Ingredient,
        related_name='recipe_ingredients'
    )
    name = models.CharField(max_length=200)
    image = models.ImageField()
    text = models.TextField()
    cooking_time = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1)]
    )

    class Meta:
        ordering = ("-id",)
        verbose_name = "Recipe"
        verbose_name_plural = "Recipes"

    def __str__(self):
        return self.name

Here is the view code:

class RecipeViewSet(ModelViewSet):
    queryset = Recipe.objects.all()
    permission_classes = [IsAdminOrAuthorOrReadOnly, ]
    serializer_class = RecipeInSerializer
    pagination_class = LimitPageNumberPagination
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['tags', ]
    filter_class = RecipeFilter

Serializer:

class RecipeOutSerializer(serializers.ModelSerializer):
    tags = ManyRelatedField(child_relation=TagSerializer())
    author = CustomUserSerializer()
    ingredients = serializers.SerializerMethodField()
    is_favorite = serializers.SerializerMethodField()
    is_in_shopping_cart = serializers.SerializerMethodField()

    class Meta:
        fields = '__all__'
        model = Recipe

    def get_ingredients(self, obj):
        ingredients = IngredientAmount.objects.filter(recipe=obj)
        return GetIngredientSerializer(ingredients, many=True).data

    def get_is_favorite(self, obj):
        request = self.context.get("request")
        if request.user.is_anonymous:
            return False
        return Favorite.objects.filter(recipe=obj, user=request.user).exists()

    def get_is_in_shopping_cart(self, obj):
        request = self.context.get("request")
        if not request or request.user.is_anonymous:
            return False
        return ShoppingCart.objects.filter(recipe=obj, user=request.user).exists()

And custom filter code:

class RecipeFilter(rest_framework.FilterSet):
    tags = ModelMultipleChoiceFilter(
        field_name='tags__slug',
        to_field_name="slug",
        queryset=Tag.objects.all()
    )

    favorite = BooleanFilter(field_name='is_favorite', method='filter_favorite')

    def filter_favorite(self, queryset, name, value):
        return queryset.filter(is_favorite__exact=True)

    class Meta:
        model = Recipe
        fields = ['tags', ]

Target is is_favorited field that return boolean value. I tried writing func in custom filter class that return queryset but didnt work, neither documentation helped me with examples. Hope for your help.


Solution

  • We can use queryset annotate:

    from django.db import models
    from rest_framework import serializers
    
    class RecipeViewSet(ModelViewSet):
        def get_queryset(self):
            user = self.request.user
            user_id = user.id if not user.is_anonymous else None
            return Recipe.objects.all().annotate(
                total_favorite=models.Count(
                    "favorite",
                    filter=models.Q(favorite__user_id=user_id)
                ),
                is_favorite=models.Case(
                    models.When(total_favorite__gte=1, then=True),
                    default=False,
                    output_field=BooleanField()
                )
            )
    
    
    class RecipeOutSerializer(serializers.ModelSerializer)
        is_favorite = serializers.BooleanField(read_only=True)
    
        class Meta:
            model = Recipe
            fields = (
                # ...
                is_favorite,
            )
    
    class RecipeFilter(rest_framework.FilterSet):
        favorite = BooleanFilter(field_name='is_favorite')