python 54 lines · 8 steps

Safely reordering tasks with a Django action

A custom DRF action validates a list of ids and atomically rewrites their positions without collisions.

Explained by highlit
1from django.db import transaction
2from django.db.models import F
3from django.http import JsonResponse
4from django.views.decorators.http import require_POST
5from rest_framework import serializers, status
6from rest_framework.decorators import action
7from rest_framework.response import Response
8from rest_framework.viewsets import ModelViewSet
9 
10from .models import Task
11 
12 
13class ReorderSerializer(serializers.Serializer):
14 ids = serializers.ListField(
15 child=serializers.IntegerField(min_value=1),
16 allow_empty=False,
17 )
18 
19 def validate_ids(self, ids):
20 if len(ids) != len(set(ids)):
21 raise serializers.ValidationError("Duplicate task ids are not allowed.")
22 return ids
23 
24 
25class TaskViewSet(ModelViewSet):
26 def get_queryset(self):
27 return Task.objects.filter(board__owner=self.request.user).order_by("position")
28 
29 @action(detail=False, methods=["post"])
30 def reorder(self, request):
31 serializer = ReorderSerializer(data=request.data)
32 serializer.is_valid(raise_exception=True)
33 ids = serializer.validated_data["ids"]
34 
35 queryset = self.get_queryset()
36 owned = set(queryset.filter(pk__in=ids).values_list("pk", flat=True))
37 missing = set(ids) - owned
38 if missing:
39 return Response(
40 {"detail": f"Unknown or forbidden ids: {sorted(missing)}"},
41 status=status.HTTP_400_BAD_REQUEST,
42 )
43 
44 with transaction.atomic():
45 locked = queryset.select_for_update().filter(pk__in=ids)
46 locked.update(position=F("position") + len(ids) + 1)
47 
48 ordered = {pk: index for index, pk in enumerate(ids)}
49 tasks = list(queryset.filter(pk__in=ids))
50 for task in tasks:
51 task.position = ordered[task.pk]
52 Task.objects.bulk_update(tasks, ["position"])
53 
54 return Response({"reordered": ids}, status=status.HTTP_200_OK)
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Validate and scope inputs to the current user before mutating anything so forbidden or unknown ids never slip through.
  2. 2Wrapping position rewrites in a transaction with select_for_update prevents concurrent reorders from corrupting order.
  3. 3Bumping positions into a temporary range first avoids unique-collision errors when swapping existing values.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Safely reordering tasks with a Django action — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code