From 293c78c52813a9a3b0006823782b338890b29d2b Mon Sep 17 00:00:00 2001 From: Konrad Mohrfeldt <konrad.mohrfeldt@farbdev.org> Date: Sun, 24 Apr 2022 16:58:57 +0200 Subject: [PATCH] feat: allow single request scheduling Until now the steering API client was expected to submit two requests. 1. The initial schedule request, with a `schedule` object 2. A second schedule request, with an additional `solutions` object This `solutions` object was empty, if no conflicts arose from the submitted schedule in the first place. But instead of just creating the requested schedule if no conflicts were detected we still required that second round-trip, introducing possible race conditions in the client, if another successful scheduling request was made between requests. From now on the steering API will create schedules if no conflicts have been detected and will only require the client to submit solutions if there really are conflicts to solve. --- program/models.py | 17 ++++++++++++++--- program/views.py | 46 ++++++++++++---------------------------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/program/models.py b/program/models.py index f11245b2..ac7677b6 100644 --- a/program/models.py +++ b/program/models.py @@ -41,6 +41,12 @@ from steering.settings import ( ) +class ScheduleConflictError(ValidationError): + def __init__(self, *args, conflicts=None, **kwargs): + super().__init__(*args, **kwargs) + self.conflicts = conflicts + + class Type(models.Model): name = models.CharField(max_length=32) slug = models.SlugField(max_length=32, unique=True) @@ -732,7 +738,7 @@ class Schedule(models.Model): """ sdl = data["schedule"] - solutions = data["solutions"] + solutions = data.get("solutions", []) # Regenerate conflicts schedule = Schedule.instantiate_upcoming(sdl, show_pk, schedule_pk) @@ -756,9 +762,10 @@ class Schedule(models.Model): ) if len(solutions) != num_conflicts: - raise ValidationError( + raise ScheduleConflictError( _("Numbers of conflicts and solutions don't match."), code="one-solution-per-conflict", + conflicts=conflicts, ) # Projected timeslots to create @@ -963,7 +970,11 @@ class Schedule(models.Model): conflicts["notes"] = data.get("notes") conflicts["playlists"] = data.get("playlists") - return conflicts + raise ScheduleConflictError( + _("Not all conflicts have been resolved."), + code="unresolved-conflicts", + conflicts=conflicts, + ) # Collect upcoming timeslots to delete which might still remain del_timeslots = TimeSlot.objects.filter( diff --git a/program/views.py b/program/views.py index 8d18a2db..c000b10b 100644 --- a/program/views.py +++ b/program/views.py @@ -42,6 +42,7 @@ from program.models import ( MusicFocus, Note, Schedule, + ScheduleConflictError, Show, TimeSlot, Topic, @@ -483,28 +484,17 @@ class APIScheduleViewSet( if show_pk is None or "schedule" not in request.data: return Response(status=status.HTTP_400_BAD_REQUEST) - # First create submit -> return projected timeslots and collisions - # TODO: Perhaps directly insert into database if no conflicts found - if "solutions" not in request.data: + try: + resolution = Schedule.resolve_conflicts(request.data, pk, show_pk) + except ScheduleConflictError as exc: # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it - return Response( - Schedule.make_conflicts(request.data["schedule"], pk, show_pk), - ) - - # Otherwise try to resolve - resolution = Schedule.resolve_conflicts(request.data, pk, show_pk) + return Response(exc.conflicts) if all(key in resolution for key in ["create", "update", "delete"]): # this is a dry-run return Response(resolution, status=status.HTTP_202_ACCEPTED) - # If resolution went well - if "projected" not in resolution: - return Response(resolution, status=status.HTTP_201_CREATED) - - # Otherwise return conflicts - # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it - return Response(resolution) + return Response(resolution, status=status.HTTP_201_CREATED) def update(self, request, *args, **kwargs): """ @@ -540,26 +530,14 @@ class APIScheduleViewSet( serializer = ScheduleSerializer(schedule) return Response(serializer.data) - # First update submit -> return projected timeslots and collisions - if "solutions" not in request.data: - # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it - return Response( - Schedule.make_conflicts( - request.data["schedule"], schedule.pk, schedule.show.pk - ) + try: + resolution = Schedule.resolve_conflicts( + request.data, schedule.pk, schedule.show.pk ) + except ScheduleConflictError as exc: + # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it + return Response(exc.conflicts) - # Otherwise try to resolve - resolution = Schedule.resolve_conflicts( - request.data, schedule.pk, schedule.show.pk - ) - - # If resolution went well - if "projected" not in resolution: - return Response(resolution) - - # Otherwise return conflicts - # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it return Response(resolution) def destroy(self, request, *args, **kwargs): -- GitLab