bluebuddah
bluebuddah

Reputation: 317

Filter on Nested serializer - Django REST

I have the following models.py:

class Question(models.Model):

    code = models.CharField(max_length=12)
    text = models.CharField(max_length=1000, null=True)
    catgeroy = models.ForeignKey(
        Category, on_delete=models.PROTECT, null=True, blank=True, db_index=True, related_name='category')


class Answer(models.Model):

    question = models.ForeignKey(
        Question, on_delete=models.PROTECT, null=True, blank=True, db_index=True, related_name='question')
    exam = models.ForeignKey(Exam, on_delete=models.CASCADE)
    value = models.FloatField(null=True, blank=True)


class Exam(models.Model):

    year = models.IntegerField()

my nested serializer looks like this:


class AnswerSerializer(serializers.ModelSerializer):

    related_name = 'answer'

    class Meta:
        model = Value
        fields = ('id', 'value', 'question', 'exam')


class NestedQuestionAnswerSerializer(serializers.ModelSerializer):

    answer = AnswerSerializer(many=True, read_only=True)

    class Meta:
        model = Question
        fields = (
            'id','code', 'text', 'answer'
        )

my views.py looks like this:

class QuestionAnswerViewSet(BaseCertViewSet):
    queryset = Question.objects.all()
    serializer_class = serializers.NestedQuestionAnswerSerializer
    filter_backends = [filters.DjangoFilterBackend]
    filterset_fields = ('category',)

my urls.py looks like this:

router.register('question-answer', views.QuestionAnswerViewSet, 'question-answer')

What I would like to be able to do is to filter by both Category AND Exam(which is a child attribute). So something like this: https://example.com/api/question-answer?category=4&exam=21

This potentially should return all the Questions that are part of category=4 AND appeared on exam=21.

I have no problem filtering by category alone, but can't seem to filter on exam which is a child foreign key.

I've tried many solutions on SO but none seem to do the above.

UPDATE:

Thanks everyone for your suggested solutions.

I ended up using this solution

Added a List Serializer class and modified the to_representation function:

class FilteredAnswerSerializer(serializers.ListSerializer):
    def to_representation(self, data):
        qry_exam = self.context['request'].GET.get('exam')
        data = data.filter(exam=qry_exam)
        return super(FilteredAnswerSerializer,  self).to_representation(data)

and then in my Answer serializer I call it:


class AnswerSerializer(serializers.ModelSerializer):

    related_name = 'answer'

    class Meta:
        model = Value

        list_serializer_class = FilteredAnswerSerializer

        fields = ('id', 'value', 'question', 'exam')

Upvotes: 1

Views: 2844

Answers (3)

damon
damon

Reputation: 15128

One way to do it would be to create a custom FilterSet for the ViewSet using django-filter.

That is the more readable, and preferred way of doing it, since the code will be clearer and easier to alter in the future.

A very simple, less extendable way of achieving this would be to override the get_queryset method of the ViewSet class.

from django.db.models import Prefetch

class NestedQuestionAnswerSerializer(serializers.ModelSerializer):
    answer = AnswerSerializer(source="filtered_answers", many=True, read_only=True)

    class Meta:
        model = Question
        fields = ('id', 'code', 'text', 'answer')

class QuestionAnswerViewSet(BaseCertViewSet):
    queryset = Question.objects.all()
    serializer_class = serializers.NestedQuestionAnswerSerializer
    filter_backends = [filters.DjangoFilterBackend]
    filterset_fields = ('category',)

    def get_exam_param(self):
        """ A helper to extract the exam id from the query_params. """
        try:
            return int(self.request.query_params["exam"])
        except (KeyError, ValueError, TypeError):
            return None

    def get_queryset(self):
        queryset = super().get_queryset()
        exam = self.get_exam_param()
        if exam is not None:
            queryset = queryset.filter(answer__exam_id=exam).prefetch_related(
                Prefetch(
                    "answers",
                    queryset=Answer.objects.filter(exam_id=exam),
                    to_attr="filtered_answers",
                ),
            )
        else:
            queryset = queryset.prefetch_related(
                Prefetch(
                    "answers",
                    queryset=Answer.objects.all(),
                    to_attr="filtered_answers",
                ),
            )
        return queryset

Edit: Added filtered_answers to get_queryset and serializer class based on updated understanding of question from comments. Mostly adapted from this answer here.

Upvotes: 2

bluebuddah
bluebuddah

Reputation: 317

Thanks everyone for your suggested solutions.

I ended up using this solution

Added a List Serializer class and modified the to_representation function:

class FilteredAnswerSerializer(serializers.ListSerializer):
    def to_representation(self, data):
        qry_exam = self.context['request'].GET.get('exam')
        data = data.filter(exam=qry_exam)
        return super(FilteredAnswerSerializer,  self).to_representation(data)

and then in my Answer serializer I call it:


class AnswerSerializer(serializers.ModelSerializer):

    related_name = 'answer'

    class Meta:
        model = Value

        list_serializer_class = FilteredAnswerSerializer

        fields = ('id', 'value', 'question', 'exam')

I will update my question with the solution.

Upvotes: 0

aman kumar
aman kumar

Reputation: 3156

you can create the filter class, in that filter class you can write down the custom fields and their query.

from django_filters import rest_framework as filters
class QuestionFilter(filters.FilterSet):
   exam = filters.IntegerField(method="filter_exam")

  class Meta:
       fields = ('category', 'exam')

  def filter_exam(self, queryset, name, value):
     return queryset.filter(answer__exam_id=value)

in view

class QuestionAnswerViewSet(BaseCertViewSet):
    queryset = Question.objects.all()
    serializer_class = serializers.NestedQuestionAnswerSerializer
    filter_backends = [filters.DjangoFilterBackend]
    filter_class = QuestionFilter

Upvotes: 0

Related Questions