How to properly match an API to Views in Django

Viewed 61

My question boils down to one of best practice for REST and Views in Django. I am reworking the REST api for my current project, and that includes reworking our Views (Controller in traditional MVC) somewhat. Currently, everything is tunneled through POST requests, even things that should be a simple GET.

From a rest perspective, a list of all jobs should be returned with a URL like:

api/jobs/

and a single job (with ID 1) should be retrieved with:

api/jobs/1

and finally this should return all tasks that are part of job 1

api/jobs/1/tasks/

This is the philosophy I'm using for the new system, but I'm torn on the implementation. Should both of these get their own View in Django, or should one view handle both.

Currently I am using one View, where all relevant urls route to it, with logic like this;

def get(self, request, id=None, task_id=None):

    if id is None:
        return Job.objects.all()  # This will be the LIST'
    if task_id is not None:
        return Job.objects.get(id=id).task_set.all()
    return Job.objects.get(id=id)

This seems like it could lead to very complex and verbose views if more complexity is added. If I wanted to add similar logic to the task logic for some other resource, would I keep adding to the view, or should each of these get its own View?

2 Answers

I don't think you should include this all into one view. I'd use two ModelViewSets and structure their urls as below:

views.py

class JobViewSet(ModelViewSet):
    serializer_class = JobSerializer
    queryset = Job.objects.all()

class TaskViewSet(ModelViewSet):
    serializer_class = TaskSerializer
    queryset = Task.objects.all()

    def get_queryset(self, request, *args, **kwargs):
        # Override get_queryset to allow filtering by `job_id` url kwarg.
        qs = self.queryset
        if kwargs.get('job_id'):
            job = Job.objects.get(pk=kwargs.get('job_id'))
            qs = qs.filter(job=job)
        return qs

urls.py

urlpatterns = [
    url(
        r'^api/jobs/$', 
        JobViewSet.as_view({'get': 'list', 'post': 'create'}
    ),
    url(
        r'^api/jobs/(?P<pk>\d+)/$', 
        JobViewSet.as_view({'get': 'retrieve', 'patch': 'update', 'patch': 'partial_update', 'delete': 'destroy'}
    ),
    url(
        r'^api/jobs/(?P<job_id>\d+)/tasks/$', 
        TaskViewSet.as_view({'get': 'list', 'post': 'create'}
    ),
    url(
        r'^api/jobs/(?P<job_id>\d+)/tasks/(?P<pk>\d+)/$', 
        TaskViewSet.as_view({'get': 'retrieve', 'patch': 'update', 'patch': 'partial_update', 'delete': 'destroy'}
    ),
]

I've avoided using Rest Framework Routers, but they essentially generate similar routes to this for single querysets, nesting querysets gets more complicated with routers. You may notice that your routes expect a pk keyword argument for the ViewSets to get a single instance of the object, this is the default lookup value for ModelViewSets. Once we nest the /tasks/ endpoint beneath the /jobs/ endpoint we have to change the jobs pk keyword argument to jobs_id so the view won't use it as the default lookup value for Tasks. jobs_id is used in the custom get_queryset method to filter appropriately.

Related