16
\$\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\$
1

1 Answer 1

1
\$\begingroup\$

If the collection is at another model then you must setup the serializer with the 'many' kwarg.

class CollectionContainerViewSet(viewsets.ModelViewSet):
    queryset = CollectionContainer.objects.all()
    collection_ids = serializers.PrimaryKeyRelatedField(
                      many=true,
                      queryset=Collection.objects.all()
                    )

Or maybe change the collection to accept updating many?

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

I guess that in most situations you don't explicitly add or remove, but you are change the collection to a known state - containing a specific set of items. If that is not the case and you really must add and remove explicitly I think it is better to have two actions, one for adding and one for removing items by id.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.