Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 53

Managing Annotation Tasks with DRF

Recommended resources

Welcome back! In our last lesson, you successfully created a management command to bulk-import image metadata from a CSV file. You now have a database populated with Image records, each waiting to be part of our dataset pipeline. This was a crucial step in automating data ingestion, a common task for a backend engineer.

Today, we transition from getting data into our system to building the interface for working with that data. We will create the API endpoints that allow users and services to manage annotation tasks. This directly addresses the core requirement from the Infilect job description: "Designs and develops REST APIs" and "experience in dataset annotation pipelines." You will build the API backbone for creating, listing, and updating the status of annotation tasks associated with the images we just imported.

By the end of this lesson, you will be able to:

  • Design and implement a Django model to represent an annotation task, including status tracking.
  • Create a DRF ModelSerializer to convert your annotation task data to and from JSON.
  • Build a complete set of CRUD API endpoints with minimal code using a DRF ModelViewSet.
  • Connect your ViewSet to your application's URLs using a DefaultRouter.
  • Test and interact with your new API endpoints using DRF's browsable API.

Let's start building the core of our annotation system.

The Magic of ViewSets and Routers

Before we write any code, let's understand a key concept in Django REST Framework that enables rapid API development: ViewSets and Routers. In the past, you might have created separate views for listing items, creating a new item, retrieving a single item, updating it, and deleting it. A ViewSet consolidates all this related logic for a single resource (like our "Annotation Task") into one class. A Router then automatically generates the URL patterns for all those actions. This combination saves you from writing a lot of repetitive boilerplate code.

Viewsets & Routers in Django REST Framework

The "BugBytes" channel has a great conceptual video on this topic. Watch the first couple of sections to understand the "why" behind this powerful pattern.

Focus on these two key ideas: What is a ViewSet? — It explains how a ViewSet is a single class that provides actions like list and create instead of traditional get and post methods. The advantages — This part highlights the two main benefits: combining repeated logic and automating URL configuration with routers.

With that conceptual foundation, let's apply it to our project.


Step 1: Modeling the Annotation Task

First, we need a way to represent an annotation task in our database. This means creating a new Django model. An annotation task needs to be linked to an image, have a status, and hold the actual annotation data.

Open your datasets/models.py file and add the following AnnotationTask model.




# datasets/models.py

from django.db import models
from django.conf import settings # We'll need this for the user




# ... your existing Image model ...
class Image(models.Model):



    # ...
    pass


class AnnotationTask(models.Model):
    """
    Represents a single annotation task for an image.
    """
    class Status(models.TextChoices):
        PENDING = 'pending', 'Pending'
        IN_PROGRESS = 'in_progress', 'In Progress'
        COMPLETED = 'completed', 'Completed'
        REJECTED = 'rejected', 'Rejected'

    image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name='annotation_tasks')
    annotator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='tasks')
    
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.PENDING,
        db_index=True
    )
    



    # JSONField is great for storing flexible, unstructured annotation data (e.g., bounding boxes, polygons)
    annotation_data = models.JSONField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f"Task for Image {self.image.id} ({self.status})"

This model introduces a few important concepts.

Build a Django REST API in 30 Minutes [2026]

The "Tech Insider" tutorial provides a modern and practical guide to building DRF APIs. We'll borrow a clean pattern for defining choices from their example.

In the section Step 4: Define Your Data Models, focus on how they define Status and Priority using TextChoices. This is a built-in Django feature that makes defining and using choices for fields like our status much cleaner and more readable than using tuples. We've used the exact same pattern.

After adding the model, create and apply the database migrations:

python manage.py makemigrations datasets
python manage.py migrate

Step 2: Serializers - The Data Bridge

Now that we have our model, we need a serializer to translate AnnotationTask instances into JSON and vice-versa. Serializers handle both converting your data for output (read operations) and validating incoming data for creation/updates (write operations).

This flowchart illustrates the two main data paths in DRF. For write requests (like POST), the serializer validates incoming data before creating a model instance. For read requests (like GET), the serializer formats the model instance into JSON for the response.

Create a new file, datasets/serializers.py, and add the following code:




# datasets/serializers.py
from rest_framework import serializers
from .models import AnnotationTask, Image

class AnnotationTaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = AnnotationTask
        fields = [
            'id', 'image', 'annotator', 'status', 
            'annotation_data', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'annotator', 'created_at', 'updated_at']

    def create(self, validated_data):



        # Automatically assign the logged-in user as the annotator when creating a task
        validated_data['annotator'] = self.context['request'].user
        return super().create(validated_data)

This AnnotationTaskSerializer is a ModelSerializer, which automatically generates fields and validators based on our AnnotationTask model.

  • fields: Specifies which model fields to include.
  • read_only_fields: Prevents clients from writing to fields that should be set by the server, like the id or timestamps.
  • We've also overridden the create method slightly to automatically associate the currently authenticated user with any new annotation they create.

Step 3: ViewSets - The API Logic

With the serializer ready, we can create the ViewSet. This class will contain the logic for handling API requests for our AnnotationTask resource.

Open datasets/views.py and add the AnnotationTaskViewSet:




# datasets/views.py
from rest_framework import viewsets, permissions
from .models import AnnotationTask
from .serializers import AnnotationTaskSerializer

class AnnotationTaskViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows annotation tasks to be viewed or edited.
    """
    queryset = AnnotationTask.objects.all().select_related('image', 'annotator')
    serializer_class = AnnotationTaskSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):



        # Users should only see their own tasks, unless they are staff
        user = self.request.user
        if user.is_staff:
            return AnnotationTask.objects.all()
        return AnnotationTask.objects.filter(annotator=user)

This is incredibly concise yet powerful. Let's break it down:

  • viewsets.ModelViewSet: This is the magic class. It automatically provides the create(), retrieve(), update(), partial_update(), destroy(), and list() actions. We get a full CRUD API for free.
  • queryset: The default set of objects for the view. We use select_related to optimize database queries by fetching related image and annotator objects in a single trip.
  • serializer_class: Tells the viewset which serializer to use.
  • permission_classes: IsAuthenticated ensures that only logged-in users can access this endpoint.
  • get_queryset(): We override this method to implement business logic. It filters the tasks so that a regular user only sees their own tasks, while a staff user can see all tasks. This is a fundamental security practice.

Step 4: Routers - Configuring the URLs

The final step is to wire our new ViewSet into our project's URL configuration.

First, create a datasets/urls.py file to hold the app-specific routes.




# datasets/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'annotation-tasks', views.AnnotationTaskViewSet, basename='annotationtask')

urlpatterns = [
    path('', include(router.urls)),
]

The DefaultRouter will generate the necessary URLs for our AnnotationTaskViewSet:

  • GET /api/datasets/annotation-tasks/: List all tasks.
  • POST /api/datasets/annotation-tasks/: Create a new task.
  • GET /api/datasets/annotation-tasks/{id}/: Retrieve a specific task.
  • PUT /api/datasets/annotation-tasks/{id}/: Update a specific task.
  • ...and so on.

Now, include this new URL file in your main project's config/urls.py:




# config/urls.py




# ... other imports
from django.urls import path, include # make sure include is imported

urlpatterns = [
    path('admin/', admin.site.urls),



    # Add this line
    path('api/datasets/', include('datasets.urls')),
]

Step 5: Testing Your API

Let's see our work in action. Start the development server:

python manage.py runserver

Before you test, make sure you have a user account. If you don't, create one:

python manage.py createsuperuser

Now, open your web browser and navigate to http://127.0.0.1:8000/api/datasets/annotation-tasks/. You will be prompted to log in. Use the superuser account you just created.

Once logged in, you will see the Django REST Framework's browsable API.

Your view will be for "Annotation Task List", but the interface is the same. It shows the JSON for existing tasks and a form to create new ones.

You can now interact with your API:

  1. List (GET): The page initially shows a list of annotation tasks, which will be empty.
  2. Create (POST): Scroll down to the HTML form. You only need to provide an image ID (use one from the images you imported). The other fields will be handled automatically or have defaults. Click "POST". You should see your new task appear in the list.
  3. Retrieve (GET): Click the hyperlink on the id of the task you just created. This will take you to its detail page (.../annotation-tasks/1/).
  4. Update (PUT/PATCH): On the detail page, scroll down to the form. Change the status from "pending" to "completed" and click "PUT". The page will refresh showing the updated status.

Conclusion

Congratulations! In just one lesson, you've designed a data model, built a serializer, and created a complete, secure, multi-user CRUD API for managing annotation tasks. You used the powerful abstractions of ModelViewSet and DefaultRouter to achieve this with remarkable efficiency. This Model -> Serializer -> ViewSet -> Router pattern is the backbone of professional DRF development.

Key Takeaways:

  • models.TextChoices provides a clean way to manage field choices like statuses.
  • ModelSerializer bridges the gap between your database models and JSON API representations.
  • ModelViewSet gives you a full set of CRUD operations for a resource with just a few lines of code.
  • DefaultRouter automatically generates standard, RESTful URL patterns for your viewsets.
  • Overriding get_queryset in a viewset is the standard way to enforce per-user data permissions.

Our API is functional, but it's still basic. In the next lesson, we will enhance it by adding powerful features like filtering, searching, and pagination, making it easier for clients to work with large numbers of tasks.