15
\$\begingroup\$

I have a model Collection which has a many to many relation to a model Item.

I want to be able to add or remove items to this collection using Django Rest Framework.

Option1 - make an action:

class CollectionViewSet(viewsets.ModelViewSet):
    queryset = Collection.objects.all()
    serializer_class = CollectionSerializer

    @action()
    def update_items(self, request, **kwargs):
        collection = self.get_object()
        add_items_id = request.DATA.pop('add_items', [])
        remove_items_id = request.DATA.pop('remove_items', [])
        items_add = Item.objects.filter(id__in=add_items_id).all()
        collection.items.add(*items_add)
        items_remove = Item.objects.filter(id__in=remove_items_id).all()
        collection.items.remove(*items_remove)
        return Response()

I see two downsides with this:

  1. I cannot make one single request to update the collection with items and other fields (not without also modifying these.
  2. I do not get the "API documentation" for free.

Option2 - override the update method and use two different serializers

class CollectionSerializer(serializers.HyperlinkedModelSerializer):
    items = ItemSerializer(many=True, read_only=True)

    class Meta:
        model = Collection
        fields = ('id', 'title', 'items')


class CollectionUpdateSerializer(serializers.HyperlinkedModelSerializer):
    add_items = serializers.PrimaryKeyRelatedField(many=True, source='items', queryset=Item.objects.all())
    remove_items = serializers.PrimaryKeyRelatedField(many=True, source='items', queryset=Item.objects.all())

    class Meta:
        model = Collection
        fields = ('id', 'title', 'add_items', 'remove_items')


class CollectionViewSet(viewsets.ModelViewSet):
    queryset = Collection.objects.all()

    def update(self, request, *args, **kwargs):
        collection = self.get_object()
        add_items_id = request.DATA.pop('add_items', [])
        remove_items_id = request.DATA.pop('remove_items', [])
        items_add = Item.objects.filter(id__in=add_items_id).all()
        collection.items.add(*items_add)
        items_remove = Item.objects.filter(id__in=remove_items_id).all()
        collection.items.remove(*items_remove)
        # fool DRF to set items to the new list of items (add_items/remove_items has source 'items')
        request.DATA['add_items'] = collection.items.values_list('id', flat=True)
        request.DATA['remove_items'] = request.DATA['add_items']
        return super().update(request, *args, **kwargs)

    def get_serializer_class(self):
        if self.request.method == "PUT":
            return CollectionUpdateSerializer
        return CollectionSerializer

Unfortunately, option2 has this horrible and inefficient hack. Here is why:

Super is called to handle the other properties (in this case only 'title'), it will see that add_items/remove_items is on the source 'items', (and at this point add_items/remove_items params would be empty) so it would remove all items.

What would be the canonical way to handle add/remove of items in a list via django-rest-framework, such that we still get: - 1 request to update the object (i.e. not via a separate action request) - re-using DRF patterns / auto-generated API

\$\endgroup\$

Browse other questions tagged or ask your own question.