Search code examples
pythondjango-modelsserializationdjango-rest-frameworkdrf-queryset

Using the serializer context in a Serializer field queryset definition


I'm looking for a way to use the serializer context defined in the ModelViewSet using the get_serializer_context to be used in the queryset declaration of a specific SlugRelatedField:

class ReservationViewSet(ViewPermissionsMixin, viewsets.ModelViewSet):
serializer_class = ReservationSerializer

def get_queryset(self):
    code = self.kwargs['project_code']
    project= Project.objects.get(code=code)
    queryset = Reservation.objects.filter(project=project)
    return queryset

def get_serializer_context(self):
    return {"project_code": self.kwargs['project_code'], 'request': self.request}

In all serializer methods this is accessible using self.context, but I would like to filter the queryset of this field using this info in the context dictionary:

class ReservationSerializer(serializers.ModelSerializer):

    project= serializers.SlugRelatedField(slug_field='code', queryset=Project.objects.all(), required=False)
    storage_location = serializers.SlugRelatedField(slug_field='description', queryset=StorageLocation.objects.filter(project__code = context['project_code'])), required=False)

Here the queryset applied to the StorageLocation (project__code = context['project_code']) is where my current issue lies.

Some additional context: this issue is an attempt to resolve the following error from the rest_framework (the StorageLocation queryset was set to .all()):

projects.models.procurement.StorageLocation.MultipleObjectsReturned: get() returned more than one StorageLocation -- it returned 2!


Solution

  • To do this you will need to create a custom field and override the behavior of either get_queryset or to_internal_value. Using get_queryset is simpler in this case, and keeps all the good validation in the base class, so we'll use that.

    This example field uses a VERY generic filter style. I've done it this way so it applies equally to whomever comes after you with a similar question.

    from typing import Optional, List
    from rest_framework.relations import SlugRelatedField
    
    
    class CustomSlugRelatedField(SlugRelatedField):
        """
        Generic slug related field, with additional filters.
        Filter functions take (queryset, context) and return a queryset
    
        >>> class MySerializer:
        >>>    field = CustomSlugRelatedField(ModelClass, 'slug', filters=[
        >>>        lambda qs, ctx: qs.filter(field=ctx["value"])
        >>>    ])
        """
    
        def __init__(self, model, slug_field: str, filters: Optional[List] = None):
            assert isinstance(filters, list) or filters is None
            super().__init__(slug_field=slug_field, queryset=model.objects.all())
            self.filters = filters or []
    
        def get_queryset(self):
            qs = super().get_queryset()
            for f in self.filters:
                qs = f(qs, self.context)
            return qs
    
    
    class MySerializer(serializers.Serializer):
        field = CustomSlugRelatedField(Product, 'slug', filters=[
            lambda q, c: q.filter(product_code=c["product_code"])
        ]) 
    
    

    Also, you should modify get_serializer_context to call super() first and add the new data on top of that.

        def get_serializer_context(self):
            ctx = super().get_serializer_context()
            ctx.update(product_code=self.kwargs['product_code'])
            return ctx