William C.
William C.

Reputation: 63

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!

Upvotes: 1

Views: 934

Answers (2)

William C.
William C.

Reputation: 63

Thanks Andrew, problem solved:

  • As this is a quite often recurring pattern in our serializers, your custom field method is the cleanest (with minor simplifications making it a bit less general)

Based on your solution I also found this way, modifying the 'get_fields' method of the serializer. Less complex, but also less clean if the pattern occurs quite often:

class ReservationSerializer(serializers.ModelSerializer):

def get_fields(self, *args, **kwargs):
    fields = super().get_fields(*args, **kwargs)
    fields['storage_location'].queryset = StorageLocation.objects.filter(project__code=self.context['project_code'])
    return fields

Upvotes: 2

Andrew
Andrew

Reputation: 8673

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

Upvotes: 2

Related Questions