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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Validate and scope inputs to the current user before mutating anything so forbidden or unknown ids never slip through.
- 2Wrapping position rewrites in a transaction with select_for_update prevents concurrent reorders from corrupting order.
- 3Bumping positions into a temporary range first avoids unique-collision errors when swapping existing values.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/safely-reordering-tasks-with-a-django-action-explained-python-2755/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.