23

Please consider these three models:

class Movie(models.Model):
    name = models.CharField(max_length=254, unique=True)
    language = models.CharField(max_length=14)
    synopsis = models.TextField()

class TimeTable(models.Model):
    date = models.DateField()

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

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

And each of them has their serializers:

class MovieSerializer(serializers.HyperlinkedModelSerializer):
    movie_id = serializers.IntegerField(read_only=True, source="id")

    class Meta:
        model = Movie
        fields = '__all__'    

class TimeTableSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = TimeTable
        fields = '__all__'


class ShowSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Show
        fields = '__all__'

And their routers

router.register(r'movie-list', views.MovieViewSet)
router.register(r'time-table', views.TimeTableViewSet)
router.register(r'show-list', views.ShowViewSet)        

Now I would like to get all the TimeTable objects (i.e. date list) by filtering all the Show objects by a specific movie object. This code seems to be the working and getting the list like I want it

m = Movie.objects.get(id=request_id)
TimeTable.objects.filter(show__movie=m).distinct()

But I have no clue how to use this in django rest framework? I tried doing this way (which I am pretty sure its wrong), and I am getting error:

views.py:

class DateListViewSet(viewsets.ModelViewSet, movie_id):
    movie = Movie.objects.get(id=movie_id)
    queryset = TimeTable.objects.filter(show__movie=movie).distinct()
    serializer_class = TimeTableSerializer

urls.py:

router.register(r'date-list/(?P<movie_id>.+)/', views.DateListViewSet)

error:

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

How can I filter using viewsets in django rest framework? Or if there is any other prefered way than please list it out. Thank you.

1
  • @一切都是空虚 Sorry, that was a typo. Updated correctly. Commented Mar 3, 2017 at 19:29

5 Answers 5

33
+100

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

Sign up to request clarification or add additional context in comments.

1 Comment

I know this is an old post(might be useful for someone, as have been for me today), but due to the recent updates of DRF 3.9 the @detail_route() has been replaced. The decorator has been replaced with action so in this case it would be @action(detail=True) according to the DRF announcements, which can be found here: Django Rest framework 3.9 announcements
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)

Comments

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

1 Comment

I think it makes more sense now.
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)

Comments

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.