ViewSets: collapsing a resource's views into one class
TaskListView and TaskDetailView from part 8 both declare the same queryset and serializer_class — two classes describing one resource. ViewSet merges that into one class for the whole resource, list and detail together.
ModelViewSet
# tasks/views.py
from rest_framework.viewsets import ModelViewSet
from .models import Task
from .serializers import TaskSerializer
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)One class, queryset and serializer_class declared exactly once, and ModelViewSet provides every operation both part 8 generic views did combined: list, create, retrieve, update, partial update, and destroy. perform_create() works identically to the generic view version from part 8 — ViewSets share the same hook methods generic views do, since ModelViewSet is actually built from the same generic view mixins underneath.
Where the URLs come from
A ViewSet isn't wired up with path() the way every view so far has been — it doesn't map cleanly to one URL pattern, since list and detail need different paths (/tasks/ vs. /tasks/1/) from the same class. Manually, it looks like this:
task_list = TaskViewSet.as_view({"get": "list", "post": "create"})
task_detail = TaskViewSet.as_view({"get": "retrieve", "put": "update", "delete": "destroy"})
urlpatterns = [
path("tasks/", task_list, name="task-list"),
path("tasks/<int:pk>/", task_detail, name="task-detail"),
].as_view({...}) here maps each HTTP method to one of the six standard actions (list, create, retrieve, update, partial_update, destroy) that ModelViewSet implements. This works, but it's exactly the repetition a ViewSet was supposed to remove — writing it by hand for every viewset in a real project defeats the point. Part 10 replaces this whole block with one line.
Custom actions beyond CRUD
from rest_framework.decorators import action
from rest_framework.response import Response
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
@action(detail=True, methods=["post"])
def mark_complete(self, request, pk=None):
task = self.get_object()
task.completed = True
task.save()
return Response(self.get_serializer(task).data)@action is what a plain generic view can't do cleanly — an operation that isn't one of the six standard ones. detail=True means it operates on a single object (self.get_object() fetches it using the same lookup the built-in retrieve action already uses) and needs a pk in the URL; detail=False would add a list-level action instead, like /tasks/completed/ with no ID. Router-generated URLs (part 10) pick this up automatically as /tasks/1/mark_complete/.
A ViewSet is the right default once a resource needs the full standard set — list, create, retrieve, update, delete — since it removes the duplicated queryset/serializer_class part 8 still had. A single-purpose endpoint that's genuinely just one operation, with no natural "resource" shape, often stays clearer as a standalone APIView or @api_view instead.
Next: Router — generating the URL patterns this part had to write by hand, automatically, from the ViewSet itself.