Peter Donovan
Peter Donovan

Reputation: 71

Django rest framework: exclude results when nested serializer is empty

I have a nested serializer that works, but I would like to exclude instances where the nested serializer is empty. The filtering I'm using on the nested serializer works, but currently this code returns all Sites, most of which have empty site_observations arrays when filters are applied. I would like to return only Sites that contain site_observations. I have tried a SerializerMethodField for site_observations but have the same issue. Using DRF 3.12

Relevant models are Site, and Observation which has FK to site, with related field=site_observations

serializers.py

class FilteredObsListSerializer(serializers.ListSerializer):
    def to_representation(self, data):
        projName = self.context["projName"]
    # this is my filter which works
        data = filter_site_observations(data, self.context["request"],
                                        projName)
        return super(FilteredObsListSerializer, self).to_representation(data)


class ObsFilterSerializer(serializers.ModelSerializer):
    class Meta:
        list_serializer_class = FilteredObsListSerializer
        model = Observation
        fields = "__all__"

class SiteSerializer(GeoFeatureModelSerializer):
    site_observations = ObsFilterSerializer(many=True)

    class Meta:
        model = Site
        geo_field = "geometry"
        fields = ("id", "name", "geometry", "site_observations")


views.py

class SiteList(generics.ListAPIView):
    queryset = Site.objects.all().order_by("pk")
    serializer_class = SiteSerializer
    
    # this is for filtering Observations on segment of an url:
    def get_serializer_context(self):
        context = super(SiteList, self).get_serializer_context()
        context.update({"projName": self.kwargs["name"]}) 
        return context

How can I exclude Sites where site_observations is an empty list? Thanks.

Upvotes: 1

Views: 995

Answers (2)

Peter Donovan
Peter Donovan

Reputation: 71

Thanks for the suggestion, which led to an answer. Not running filter_site_observations in the serializer worked. I reworked the filter to run in the view and got the results I needed. (I had tried this before using a SerializerMethodField but couldn't filter it properly; the nested serializer seems a better approach.) Thanks for the suggestions! Here's the filter:

def filter_site_observations(queryset, request, projName):
    '''
    filters a collection on project, observer status
    '''
    if request.user.is_authenticated:
        authprojects = [
            item.project.name
            for item in ObserverStatus.objects.filter(observer=request.user.id)
        ]
        if projName in authprojects:
            return queryset.filter(site_observations__project__name=projName)
        else:
            return queryset.filter(
                site_observations__project__name=projName).filter(
                    site_observations__private=False)
    else:
        return queryset.filter(
            site_observations__project__name=projName).filter(
                site_observations__private=False)

and the views.py:

class SiteList(generics.ListAPIView):
    serializer_class = SiteSerializer

    def get_serializer_class(self, *args, **kwargs):
        if self.request.method in ("POST", "PUT", "PATCH"):
            return SitePostSerializer
        return self.serializer_class

    def get_queryset(self):
        projName = self.kwargs["name"]
        queryset = Site.objects.all().order_by('pk')
        queryset = filter_site_observations(queryset, self.request, projName)
        queryset = queryset.filter(
            site_observations__isnull=False, ).distinct()
        queryset = self.get_serializer_class().setup_eager_loading(queryset)
        return queryset

UPDATE AUG 28 Actually, to get the filtering I needed, I had to run pretty much the same filter in both the Observation serializer AND in the SiteList views.py. This was true regardless of whether I used SerializerMethodField or a simple nested serializer for the child data. Otherwise, I would get either: 1) ALL Sites, including ones that didn't have any Observations, or 2) Sites that had some non-private Observations, but also displayed all the private ones.

filters.py

from users.models import ObserverStatus


def filter_site_observations(queryset, request, projName):
    '''
    filters a collection on project, observer status; used in views.py.
    Both of these filters seem to be required to get proper filtering.
    '''
    queryset = queryset.filter(site_observations__project__name=projName)

    if request.user.is_authenticated:
        authprojects = [
            item.project.name
            for item in ObserverStatus.objects.filter(observer=request.user.id)
        ]
        if projName in authprojects:
            return queryset
        else:
            return queryset.filter(site_observations__private=False)
    else:
        return queryset.filter(site_observations__private=False)


def filter_observations(queryset, request, projName):
    '''
    filters observations in ObsSerializer on observer status
    '''
    if request.user.is_authenticated:
        authprojects = [
            item.project.name
            for item in ObserverStatus.objects.filter(observer=request.user.id)
        ]
        if projName in authprojects:
            return queryset
        else:
            return queryset.filter(private=False)
    else:
        return queryset.filter(private=False)

So I'm filtering twice and not sure why this needs to happen.

Upvotes: 0

Brian Destura
Brian Destura

Reputation: 12068

One approach is to tell your view to only work with certain objects that meet some criteria:

class SiteList(generics.ListAPIView):
    queryset = Site.objects.filter(
        site_observations__isnull=False,
    ).distinct().order_by('pk')

This will tell SiteList to only work with Site objects that have site_observation relations existing. You need the distinct call here as described by this.

Upvotes: 2

Related Questions