From 3b00a352a5c71747175f7c5d5eb6c0720592e9b8 Mon Sep 17 00:00:00 2001 From: Konrad Mohrfeldt <konrad.mohrfeldt@farbdev.org> Date: Thu, 23 Jan 2025 15:52:37 +0100 Subject: [PATCH] feat: add reorder view mixin This mixin factory makes it easy to create atomic reorder operations for entities that have a static numeric order criteria that is supposed to be updated by clients. --- program/utils.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/program/utils.py b/program/utils.py index ca5e478..b07758f 100644 --- a/program/utils.py +++ b/program/utils.py @@ -24,7 +24,13 @@ from datetime import date, datetime, time from typing import Any, Callable, Dict, Hashable, Optional, Tuple, Union import requests +from drf_spectacular.utils import OpenApiExample, extend_schema +from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response +from django import db from django.conf import settings from django.utils import timezone from program.models import Episode, EpisodeLink, Profile, ProfileLink, Show, ShowLink @@ -247,3 +253,88 @@ def saves_relationships_to(*field_names): return wrapper return relationship_update_decorator + + +def reorder_mixin_factory( + order_field_name: str | None = None, + id_field_type: type[serializers.Field] = serializers.IntegerField, + serializer_class: type[serializers.Serializer] | None = None, +): + """ + Factory function that creates a mixin for handling bulk reordering of objects in a ViewSet. + + :param order_field_name: the field that stores the ordinal + :param id_field_type: the field type that stores the id + :param serializer_class: Optional custom response serializer for the endpoint + :returns: the parameterized mixin class for the ViewSet + """ + + class ReorderItemSerializer(serializers.Serializer): + id = id_field_type() + order = serializers.IntegerField(source=order_field_name) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Get the model from the parent's context if available + if self.parent and getattr(self.parent, "context", None): + viewset = self.parent.context.get("view") + if viewset: + self.fields["id"].queryset = viewset.get_queryset() + + class ReorderRequestSerializer(serializers.Serializer): + orderings = ReorderItemSerializer(many=True) + + class ReorderMixin: + @extend_schema( + methods=["patch"], + request=ReorderRequestSerializer, + responses={200: serializer_class} if serializer_class else {204: None}, + examples=[ + OpenApiExample( + "Reorder Example", + value={ + "orderings": [ + {"id": 1, "order": 5}, + {"id": 2, "order": 2}, + {"id": 3, "order": 1}, + ] + }, + description="Example request to reorder multiple objects", + ) + ], + description="Reorder multiple objects in a single operation.", + summary="Bulk reorder objects", + ) + @action( + detail=False, + methods=["patch"], + url_path="reorder", + serializer_class=ReorderRequestSerializer, + ) + def reorder(self, request: Request) -> Response: + serializer = ReorderRequestSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + new_orderings = serializer.validated_data["orderings"] + requested_obj_ids = [ordering["id"] for ordering in new_orderings] + model = self.get_queryset().model + obj_ids = model.objects.filter(id__in=requested_obj_ids).values_list("id", flat=True) + update_map = { + ordering["id"]: ordering["order"] + for ordering in new_orderings + if ordering["id"] in obj_ids + } + + with db.transaction.atomic(): + objects = model.objects.select_for_update().filter(id__in=update_map.keys()) + + for pk, order in update_map.items(): + objects.filter(id=pk).update(**{order_field_name or "order": order}) + + if serializer_class: + updated_objects = self.get_queryset().filter(id__in=update_map.keys()) + response_serializer = serializer_class(updated_objects, many=True) + return Response(response_serializer.data) + return Response(status=204) + + return ReorderMixin -- GitLab