71

I am using Django REST framework and have been trying to create a view that returns a small bit of information, as well as register it on my router.

I have four models which store information, and all of them have a created_time field. I am trying to make a view that returns the most recent objects (based on the created_time) in a single view, where only the four creation times are returned.

So, a possible JSON output from the view would look like

{
    "publish_updatetime": "2015.05.20 11:53",
    "meeting_updatetime": "2015.05.20 11:32",
    "training_updatetime": "2015.05.20 15:25",
    "exhibiting_updatetime": "2015.05.19 16:23"
}

I am also hoping to register this view on my router, so it appears with the rest of my endpoints when the API root is loaded.

router.register(r'updatetime', views.UpdateTimeView)

Here are the four models that I am trying to work with

class Publish(models.Model):
    user = models.ForeignKey(MyUser)
    name = models.CharField(max_length=50)
    created_time = models.DateTimeField( default=datetime.now)

class Meeting(models.Model):
    user = models.ForeignKey(MyUser)
    name = models.CharField(max_length=50)
    file_addr = models.FileField(upload_to=get_file_path)
    created_time = models.DateTimeField(default=datetime.now)

class Training(models.Model):
    user = models.ForeignKey(MyUser)
    name = models.CharField(max_length=50)
    image = models.ImageField(upload_to=get_file_path, max_length=255)
    created_time = models.DateTimeField(default=datetime.now)

class Exhibiting(models.Model):
    user = models.ForeignKey(MyUser)
    name = models.CharField(max_length=50)
    file_addr = models.FileField(upload_to=get_file_path)
    created_time = models.DateTimeField(default=datetime.now)

Is it possible to do this? And how would it be done?

Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
flytofuture
  • 969
  • 1
  • 8
  • 13
  • 2
    Do you have any views, serializers, or models written yet that could be included in your question? It's also not entirely clear what your main question is, as there's not much here to work from. – Kevin Brown-Silva May 22 '15 at 11:44
  • Hi Kevin, I have add all the models and serializers. How to use viewsets to handle this and registry the url using "router". Thanks! – flytofuture May 25 '15 at 01:43
  • It's still not clear how all of these models are supposed to be grouped. Clearly it's not by the `created_time` (that would result in them all being the same). How are these four models supposed to be retrieved? How would it normally be done in a view? – Kevin Brown-Silva May 25 '15 at 01:45
  • just want return the latest created_time for these four models. The return json include 4 tiems: publish_updatetime -> Publish.created_time, meeting_updatetime -> Meeting.created_time, training_updatetime -> Training.created_time, exhibiting_updatetime->Exhibiting.created_time. We can get the latest created_time like: Publish.objects.latest('created_time'). – flytofuture May 25 '15 at 02:13
  • Perfect, all of this was information that should have been included in the first place. Now I realize that your question is closer to "How can I have a viewset which only returns the list method" instead of "how can I aggregate multiple models into a single response". – Kevin Brown-Silva May 25 '15 at 02:46
  • Yes, you are right. change the title. – flytofuture May 25 '15 at 03:03

2 Answers2

91

Routers work with a ViewSet and aren't designed for normal views, but that doesn't mean that you cannot use them with a normal view. Normally they are used with models (and a ModelViewSet), but they can be used without them using the GenericViewSet (if you would normally use a GenericAPIView) and ViewSet (if you would just use an APIView).

For a list view, the request methods are mapped to ViewSet methods like this

  • GET -> list(self, request, format=None)
  • POST- > create(self, request, format=None)

For detail views (with a primary key in the url), the request methods use the following map

  • GET -> retrieve(self, request, pk, format=None)
  • PUT -> update(self, request, pk, format=None)
  • PATCH -> partial_update(self, request, pk, format=None)
  • DELETE -> destroy(self, request, pk, format=None)

So if you want to use any of these request methods with your view on your router, you need to override the correct view method (so list() instead of get()).


Now, specifically in your case you would have normally use an APIView that looked like

class UpdateTimeView(APIView):

    def get(self, request, format=None):
        latest_publish = Publish.objects.latest('created_time')
        latest_meeting = Meeting.objects.latest('created_time')
        latest_training = Training.objects.latest('created_time')
        latest_exhibiting = Exhibiting.objects.latest('created_time')

        return Response({
            "publish_updatetime": latest_publish.created_time,
            "meeting_updatetime": latest_meeting.created_time,
            "training_updatetime": latest_training.created_time,
            "exhibiting_updatetime": latest_exhibiting.created_time,
        })

The comparable ViewSet would be

class UpdateTimeViewSet(ViewSet):

    def list(self, request, format=None):
        latest_publish = Publish.objects.latest('created_time')
        latest_meeting = Meeting.objects.latest('created_time')
        latest_training = Training.objects.latest('created_time')
        latest_exhibiting = Exhibiting.objects.latest('created_time')

        return Response({
            "publish_updatetime": latest_publish.created_time,
            "meeting_updatetime": latest_meeting.created_time,
            "training_updatetime": latest_training.created_time,
            "exhibiting_updatetime": latest_exhibiting.created_time,
        })

Notice the two required changes: APIView -> ViewSet and get -> list. I also updated the name to indicate that it was more than just a normal view (as a ViewSet cannot be initialized the same way), but that's not required.

So with this new view, you can just register it in the router the same way as any other. You need a base_name here so the url names can be generated (normally this would pull from the queryset).

router.register(r'updatetime', views.UpdateTimeViewSet, base_name='updatetime')

So now the updatetime endpoint will be made available in the API root and you can get the latest times by just calling the endpoint (a simple GET request).

Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
  • 10
    so it is not possible to register `APIView` in a `router`? – avi Nov 04 '15 at 06:51
  • 8
    My case is that, I would like to expose several APIs that do not work with a concrete resource (defined as a Django model), and I also want to put them into one ViewSet and expose them under a specific url prefix, such as `scheduler/get_job`, `scheduler/report_result`, etc. How does your solution apply to this case? – Chenxiong Qi Nov 23 '15 at 05:08
  • @ChenxiongQi I have this exact question- did you ever find an answer? – dkhaupt Feb 06 '16 at 15:55
  • 1
    You can use the `@list_route` decorator in those cases @dkhaupt @chenxiongqi – Kevin Brown-Silva Feb 06 '16 at 16:04
  • 1
    Just want to notice, that you should use `from rest_framework import viewsets.ViewSet` to be able to use it. – yestema Feb 14 '19 at 12:53
  • 1
    Can I still parse more complicated urls that way, for instance one like `myurl//`, or the routeur can only take simple strings? – tobiasBora Jun 13 '21 at 01:45
2

There is one more way to do it:

urlpatterns = [
    # ... 
    url(r'^you_path/', include(router.urls)),
    url(r'^you_path/you_sub_path', views.UpdateTimeView.as_view()),
    # ... 
]

But stuff like Swagger will not work with that

Alexander
  • 455
  • 6
  • 11