velis
velis

Reputation: 10055

Strange ModelViewSet behaviour

I'm trying to serialize a MPTT tree model with DRF.

My code:

class SiteTreeCalc(serializers.Field):
    def to_representation(self, value):
        return value.exists()  # return True if has children, False otherwise

class SiteTreeSerializer(serializers.ModelSerializer):
    children = SiteTreeCalc()
    class Meta:
        model = SiteTree
        fields = ('id', 'site', 'children')
        depth = 1

class SiteTreeViewSet(viewsets.ModelViewSet):
    #queryset = SiteTree.objects.all()
    serializer_class = SiteTreeSerializer

    def get_queryset(self):
        if 'pk' not in self.kwargs:
            # return first-level nodes
            return SiteTree.objects.filter(level=0)
        else:
            # return all children of a given node
            return SiteTree.objects.filter(parent__id=int(self.kwargs['pk']))


router = routers.DefaultRouter()
router.register(r'rest/sitetree', SiteTreeViewSet, "SiteTreeRoots")
router.register(r'rest/sitetree/(?P<tree_id>\d+)/$', SiteTreeViewSet, "SiteTreeChildren")

I have two issues with this code:

  1. I have declared parameter "tree_id" in router registration. However, get_queryset says that parameter name is pk
  2. The second filter never works (the one that should return children of given parent). DRF returns "detail": "Not found.". If I test that line in debugger, it naturally returns all children of the given parent.

I seem to be doing something wrong, but the code seems so obvious to me that I just can't see it.

Help - as always - very appreciated.

Upvotes: 3

Views: 123

Answers (2)

velis
velis

Reputation: 10055

Turns out, I wanted to forget the convenient functionality of DefaultRouter the first chance I got.

The problem was that I wanted to create a ViewSet just like any other writable ViewSet, but this particular one was intended only for retrieving items. At least, that's what I intended. But DRF couldn't know that, so my problem #2 was a result of DRF actually checking that I'm returning ONE item with EXACTLY the same pk as was given in the URL.

A solution that works goes like this (as suggested in the DRF ViewSets documentation):

class SiteTreeViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = SiteTree.objects.filter(level=0)
    serializer_class = SiteTreeSerializer

    @detail_route()
    def children(self, request, pk=None):
        data = SiteTree.objects.filter(parent__id=int(pk))
        data = self.get_serializer(data, many=True)
        return Response(data.data)

This solution returns first-level items in default mode and also accepts /{pk}/children to return children of the given pk node. Naturally, default operations will still return just the pk node when provided with a /{pk}/ URL.

Router registration remains only the default one:

router.register(r'rest/sitetree', SiteTreeViewSet)

Upvotes: 1

Linovia
Linovia

Reputation: 20996

As for 1. you need to set lookup_url_kwarg (the named argument in the urls) on the viewset so it maps to the tree_id.

Note that routers do define the dynamic url part themselves.

As for 2. it's most of the time sending JOSN POST data with form content type. Ensure you are sending the right content type in your request's header.

Edit: Daniel has the point for 1. With your current url patterns, there's no way to distinguish a detailed top node and a list for the child node.

Upvotes: 0

Related Questions