0

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!

William C.
  • 43
  • 4

2 Answers2

2

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
Andrew
  • 8,322
  • 2
  • 47
  • 70
  • Thanks Andrew, your generic example already explains much: If i'm right, the context dict is passed down from the serializer to its fields, where it can be used to create the required queries by modifying the get_queryset method of our custom slugrelatedfield. in that case I can also use django.db.models.Q to pass custom filters into the context and apply them here? – William C. Feb 05 '21 at 15:19
  • You can filter the queryset however you want, yes. My opinion is to do your filtering in the filter, and not pass around pre-constructed `Q` objects in the context. – Andrew Feb 08 '21 at 13:31
1

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
William C.
  • 43
  • 4