[Django]-Filtering using viewsets in django rest framework

31👍

ModelViewSet by design assumes that you want to implement a CRUD(create, update, delete)
There is also a ReadOnlyModelViewSet which implements only the GET method to read only endpoints.
For Movie and Show models, a ModelViewSet or ReadOnlyModelViewSet is a good choice whether you want implement CRUD or not.
But a separate ViewSet for a related query of a TimeTable which describes a Movie model’s schedule doesn’t looks so good.
A better approach would be to put that endpoint to a MovieViewSet directly. DRF provided it by @detail_route and @list_route decorators.

from rest_framework.response import Response
from rest_framework.decorators import detail_route

class MovieViewSet(viewsets.ModelViewset):

    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

    @detail_route()
    def date_list(self, request, pk=None):
        movie = self.get_object() # retrieve an object by pk provided
        schedule = TimeTable.objects.filter(show__movie=movie).distinct()
        schedule_json = TimeTableSerializer(schedule, many=True)
        return Response(schedule_json.data)

This endpoint will be available by a movie-list/:id/date_list url
Docs about extra routes

10👍

Register your route as

router.register(r'date-list', views.DateListViewSet)

now change your viewset as shown below,

class DateListViewSet(viewsets.ModelViewSet):
    queryset = TimeTable.objects.all()
    serializer_class = TimeTableSerializer
    lookup_field = 'movie_id'

    def retrieve(self, request, *args, **kwargs):
        movie_id = kwargs.get('movie_id', None)
        movie = Movie.objects.get(id=movie_id)
        self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
        return super(DateListViewSet, self).retrieve(request, *args, **kwargs)

Use a retrieve method, which will match any GET requests to endpoint /date-list/<id>/.

Advantage is that you don’t have to explicitly handle the serialization and returning response you make ViewSet to do that hard part. We are only updating the queryset to be serialized and rest framework does the rest.

Since ModelViewSet is implemented as,

class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass

Its implementation includes the following methods (HTTP verb and endpoint on bracket)

  • list() (GET /date-list/)
  • create()(POST /date-list/)
  • retrieve()(GET date-list/<id>/)
  • update() (PUT /date-list/<id>/)
  • partial_update() (PATCH, /date-list/<id>/
  • destroy() (DELETE /date-list/<id>/)

If you want only to implement the retrieve() (GET requests to endpoint date-list/<id>/), you can do this instead of a `ModelViewSet),

from rest_framework import mixins, views

class DateListViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    queryset = TimeTable.objects.all()
    serializer_class = TimeTableSerializer
    lookup_field = 'movie_id'

    def retrieve(self, request, *args, **kwargs):
        movie_id = kwargs.get('movie_id', None)
        movie = Movie.objects.get(id=movie_id)
        self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
        return super(DateListViewSet, self).retrieve(request, *args, **kwargs)

9👍

The error

class DateListViewSet(viewsets.ModelViewSet, movie_id): NameError: name ‘movie_id’ is not defined

happens because movie_id is being passed as parent class of DataListViewSet and not as parameter as you imagined

This example in the documentation should be what you are looking for.

Adjust your URL:

url(r'date-list/(?P<movie_id>.+)/', views.DateListView.as_view())

Adjust your Model:

class Show(models.Model):
   day = models.ForeignKey(TimeTable, related_name='show')
   time = models.TimeField(choices=CHOICE_TIME)
   movie = models.ForeignKey(Movie)

class Meta:
    unique_together = ('day', 'time')

Your view would look like this:

class DateListView(generics.ListAPIView):
     serializer_class = TimeTableSerializer

     def get_queryset(self):

         movie = Movie.objects.get(id=self.kwargs['movie_id'])

         return TimeTable.objects.filter(show__movie=movie).distinct()

Another way to do it would be:

Adjust your URL:

router.register(r'date-list', views.DateListViewSet)

Adjust your Model:

class Show(models.Model):
   day = models.ForeignKey(TimeTable, related_name='show')
   time = models.TimeField(choices=CHOICE_TIME)
   movie = models.ForeignKey(Movie)

class Meta:
    unique_together = ('day', 'time')

Your view would look like this:

class DateListViewSet(viewsets.ModelViewSet):
     serializer_class = TimeTableSerializer
     queryset = TimeTable.objects.all()
     filter_backends = (filters.DjangoFilterBackend,)
     filter_fields = ('show__movie_id')

Which will allow you to make requests such as:

http://example.com/api/date-list?show__movie_id=1

See documentation

4👍

Ivan Semochkin has the correct answer but the detail decorator is deprecated. It was replaced by the action decorator.

from rest_framework.decorators import action

class MovieViewSet(viewsets.ModelViewset):

   @action(detail=True)
   def date_list(self, request, pk=None):
       movie = self.get_object() # retrieve an object by pk provided
       schedule = TimeTable.objects.filter(show__movie=movie).distinct()
       schedule_json = TimeTableSerializer(schedule, many=True)
       return Response(schedule_json.data)
👤LonnyT

0👍

To improve @all-is-vanity answer, you can explicitly use movie_id as a parameter in the retrieve function since you are overriding the lookup_field class property:

def retrieve(self, request, movie_id=None):
    movie = Movie.objects.get(id=movie_id)
    self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
    return super(DateListViewSet, self).retrieve(request, *args, **kwargs)

You can also call self.get_object() to get the object:

def retrieve(self, request, movie_id=None):
    movie = self.get_object()
    self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
    return super(DateListViewSet, self).retrieve(request, *args, **kwargs)
👤busla

Leave a comment