finngu
finngu

Reputation: 527

ViewSet and additional retrieve URL

I have a django model Donation that I expose as a ViewSet. Now I want to add an additional URL to a second model Shop where a related instance of Donation can be retrieved via the parameter order_id and custom actions can be executed.

# models.py
class Donation(models.Model):
  id = models.AutoField(primary_key=True)
  order_id = models.StringField(help_text='Only unique in combination with field `origin`')
  origin = models.ForeignKey('Shop', on_delete=models.PROTECT)

class Shop(models.Model):
  id = models.AutoField(primary_key=True)
  

# views.py
class DonationViewSet(mixins.CreateModelMixin,
                     mixins.RetrieveModelMixin,
                     mixins.ListModelMixin,
                     viewsets.GenericViewSet):

  def retrieve(self, request, *args, **kwargs):  
    if kwargs['pk'].isdigit():
      return super(DonationViewSet, self).retrieve(request, *args, **kwargs)
    else:
      shop_id = self.request.query_params.get('shop_id', None)
      order_id = self.request.query_params.get('order_id', None)
      
      if shop_id is not None and order_id is not None:
        instance = Donations.objects.filter(origin=shop_id, order_id=order_id).first()
        if instance is None:
          return Response(status=status.HTTP_404_NOT_FOUND)
        
        return Response(self.get_serializer(instance).data)

      return Response(status=status.HTTP_404_NOT_FOUND)

  @action(methods=['post'], detail=True)
  def custom_action(self, request, *args, **kwargs):
      pass

class ShopViewSet(viewsets.ModelViewSet):
  pass

# urls.py
router = routers.DefaultRouter()

router.register(r'donations', DonationViewSet)
router.register(r'shops', ShopViewSet)
router.register(r'shops/(?P<shop_id>[0-9]+)/donations/(?P<order_id>[0-9]+)', DonationViewSet)

My goal is to have http://localhost:8000/donations point at the entire DonationViewSet. Also I would like to lookup an individual donation, by its combination of shop_id and order_id like follows http://localhost:8000/shops/123/donations/1337/ and also executing the custom action like follows http://localhost:8000/shops/123/donations/1337/custom_action/. The problem I have is that the second url returns an entire queryset, not just a single instance of the model.

Upvotes: 6

Views: 2670

Answers (3)

enjoi4life411
enjoi4life411

Reputation: 694

You can add urls by simply appending to the router's urls in the config like so. If all you want to do is add a single action from a view for one specifc url, and dont need all of the actions/urls for the viewset

# urls.py
urlpatterns = [
    path('some_path', my_lookup_view)),
] # typical django url convention
urlpatterns += router.urls

# views.py
@api_view(['POST'])
def my_looup_view(request, shop_id, order_id):
    # ...some lookup...
    pass

Upvotes: 4

Brian Destura
Brian Destura

Reputation: 12068

You can also use drf-nested-routers, which will have something like this:

from rest_framework_nested import routers

from django.conf.urls import url

# urls.py
router = routers.SimpleRouter()
router.register(r'donations', DonationViewSet, basename='donations')
router.register(r'shops', ShopViewSet, basename='shops')

shop_donations_router = routers.NestedSimpleRouter(router, r'', lookup='shops')
shop_donations_router.register(
    r'donations', ShopViewSet, basename='shop-donations'
)

# views.py
class ShopViewSet(viewsets.ModelViewSet):
    def retrieve(self, request, pk=None, donations_pk=None):
        # pk for shops, donations_pk for donations

    @action(detail=True, methods=['PUT'])
    def custom_action(self, request, pk=None, donations_pk=None):
        # pk for shops, donations_pk for donations

This is not tested! But in addition to what you already have, this will support:

donations/
donations/1337/
shops/123/donations/1337/
shops/123/donations/1337/custom_action

Upvotes: 5

AKX
AKX

Reputation: 169184

You'll want to

  • derive from GenericViewSet since you're using models anyway
  • override get_object() instead with your custom lookup logic:
from rest_framework import mixins
from rest_framework.generics import get_object_or_404
from rest_framework.viewsets import GenericViewSet


class MyModelViewSet(
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.ListModelMixin,
    GenericViewSet,
):
    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())
        lookup_value = self.kwargs["pk"]
        if lookup_value.isdigit():
            obj = get_object_or_404(queryset, pk=lookup_value)
        else:
            obj = get_object_or_404(queryset, third_party_service_id=lookup_value)

        self.check_object_permissions(self.request, obj)
        return obj

    @action(methods=["post"], detail=True)
    def custom_action(self, request, *args, **kwargs):
        thing = self.get_object()


# urls.py
router.register(r"mymodels", MyModelViewSet)

This should let you do

  • mymodels/ to list models,
  • mymodels/123/ to get a model by PK,
  • mymodels/kasjdfg/ to get a model by third-party service id,
  • mymodels/123/custom_action/ to run custom_action on a model by PK
  • mymodels/kasjdfg/custom_action/ to run custom_action on a model by service id,

Upvotes: 0

Related Questions