Search code examples
pythondjangodjango-rest-frameworkdjango-querysetdjango-serializer

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.


Solution

  • 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.