From 37f858c2778eea4f5ee9a552cca3f0f76bae9de1 Mon Sep 17 00:00:00 2001
From: Ernesto Rico Schmidt <ernesto@helsinki.at>
Date: Mon, 14 Nov 2022 18:15:55 -0400
Subject: [PATCH] Avoid IndexError exception

Only return the next repetition timeslot if there is one

Fixes #131
---
 poetry.lock      | 38 ++++++++++++++---------------
 program/views.py | 63 ++++++++++++++++--------------------------------
 2 files changed, 40 insertions(+), 61 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 0148ba0c..c7b4ed59 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -229,7 +229,7 @@ sidecar = ["drf-spectacular-sidecar"]
 
 [[package]]
 name = "exceptiongroup"
-version = "1.0.0"
+version = "1.0.3"
 description = "Backport of PEP 654 (exception groups)"
 category = "dev"
 optional = false
@@ -393,7 +393,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
 
 [[package]]
 name = "pathspec"
-version = "0.10.1"
+version = "0.10.2"
 description = "Utility library for gitignore style pattern matching of file paths."
 category = "dev"
 optional = false
@@ -413,15 +413,15 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 
 [[package]]
 name = "platformdirs"
-version = "2.5.2"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+version = "2.5.4"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
-test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
+test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
 
 [[package]]
 name = "pluggy"
@@ -648,7 +648,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "setuptools"
-version = "65.5.0"
+version = "65.5.1"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "main"
 optional = false
@@ -656,7 +656,7 @@ python-versions = ">=3.7"
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
@@ -714,7 +714,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.16.6"
+version = "20.16.7"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
@@ -830,8 +830,8 @@ drf-spectacular = [
     {file = "drf_spectacular-0.24.2-py3-none-any.whl", hash = "sha256:b276e6f7bda6dfb911e742dab87c6e97bc67da2dafe82d6fd8df7cec6c8b03ec"},
 ]
 exceptiongroup = [
-    {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"},
-    {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"},
+    {file = "exceptiongroup-1.0.3-py3-none-any.whl", hash = "sha256:6002703c7d31fb9950ddc8780840f67880c440895dc1151dd551553aa1246e4a"},
+    {file = "exceptiongroup-1.0.3.tar.gz", hash = "sha256:76cac74b5207c5997678a1c7105cb6f14213c9c63c096a38cfcb529d83ce5c02"},
 ]
 filelock = [
     {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
@@ -889,8 +889,8 @@ packaging = [
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
 ]
 pathspec = [
-    {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
-    {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
+    {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"},
+    {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"},
 ]
 Pillow = [
     {file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"},
@@ -954,8 +954,8 @@ Pillow = [
     {file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"},
 ]
 platformdirs = [
-    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
-    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
+    {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"},
+    {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"},
 ]
 pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
@@ -1189,8 +1189,8 @@ requests = [
     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
 ]
 setuptools = [
-    {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
-    {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
+    {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"},
+    {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"},
 ]
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1217,6 +1217,6 @@ urllib3 = [
     {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
 ]
 virtualenv = [
-    {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"},
-    {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"},
+    {file = "virtualenv-20.16.7-py3-none-any.whl", hash = "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29"},
+    {file = "virtualenv-20.16.7.tar.gz", hash = "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e"},
 ]
diff --git a/program/views.py b/program/views.py
index acd66207..d442fb40 100644
--- a/program/views.py
+++ b/program/views.py
@@ -81,14 +81,10 @@ def json_day_schedule(request, year=None, month=None, day=None):
     if year is None and month is None and day is None:
         today = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0)))
     else:
-        today = timezone.make_aware(
-            datetime.combine(date(year, month, day), time(0, 0))
-        )
+        today = timezone.make_aware(datetime.combine(date(year, month, day), time(0, 0)))
 
     timeslots = (
-        TimeSlot.objects.get_24h_timeslots(today)
-        .select_related("schedule")
-        .select_related("show")
+        TimeSlot.objects.get_24h_timeslots(today).select_related("schedule").select_related("show")
     )
     schedule = []
     for ts in timeslots:
@@ -317,9 +313,7 @@ class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
             return Response(status=status.HTTP_401_UNAUTHORIZED)
 
         show = self.get_object()
-        serializer = ShowSerializer(
-            show, data=request.data, context={"user": request.user}
-        )
+        serializer = ShowSerializer(show, data=request.data, context={"user": request.user})
 
         if serializer.is_valid():
             # Common users mustn't edit the show's name
@@ -513,9 +507,7 @@ class APIScheduleViewSet(
         schedule = self.get_object()
 
         # If default playlist id or repetition are given, just update
-        if default_playlist_id := request.data.get("schedule").get(
-            "default_playlist_id"
-        ):
+        if default_playlist_id := request.data.get("schedule").get("default_playlist_id"):
             schedule.default_playlist_id = int(default_playlist_id)
             schedule.save()
 
@@ -530,9 +522,7 @@ class APIScheduleViewSet(
             return Response(serializer.data)
 
         try:
-            resolution = Schedule.resolve_conflicts(
-                request.data, schedule.pk, schedule.show.pk
-            )
+            resolution = Schedule.resolve_conflicts(request.data, schedule.pk, schedule.show.pk)
         except ScheduleConflictError as exc:
             return Response(exc.conflicts, status.HTTP_409_CONFLICT)
 
@@ -592,9 +582,8 @@ class APITimeSlotViewSet(
     def update(self, request, *args, **kwargs):
         show_pk = get_values(self.kwargs, "show_pk")
 
-        if (
-            not request.user.is_superuser
-            and show_pk not in request.user.shows.values_lis("id", flat=True)
+        if not request.user.is_superuser and show_pk not in request.user.shows.values_lis(
+            "id", flat=True
         ):
             return Response(status=status.HTTP_401_UNAUTHORIZED)
 
@@ -606,9 +595,10 @@ class APITimeSlotViewSet(
             # Return the next repetition
             # We do this because the Dashboard needs to update the repetition timeslot as well
             # but with another playlist containing the recording instead of the original playlist
-            ts = TimeSlot.objects.filter(show=show_pk, start__gt=timeslot.start)[0]
-            if ts.is_repetition:
-                serializer = TimeSlotSerializer(ts)
+            if first_repetition := TimeSlot.objects.filter(
+                show=show_pk, start__gt=timeslot.start, is_repetition=True
+            ).first():
+                serializer = TimeSlotSerializer(first_repetition)
                 return Response(serializer.data)
 
             # ...or nothing if there isn't one
@@ -662,9 +652,8 @@ class APINoteViewSet(
         """
         show_pk, timeslot_pk = get_values(self.kwargs, "show_pk", "timeslot_pk")
 
-        if (
-            not request.user.is_superuser
-            and show_pk not in request.user.shows.values_list("id", flat=True)
+        if not request.user.is_superuser and show_pk not in request.user.shows.values_list(
+            "id", flat=True
         ):
             return Response(status=status.HTTP_401_UNAUTHORIZED)
 
@@ -674,9 +663,7 @@ class APINoteViewSet(
         )
 
         if serializer.is_valid():
-            hosts = Host.objects.filter(
-                shows__in=request.user.shows.values_list("id", flat=True)
-            )
+            hosts = Host.objects.filter(shows__in=request.user.shows.values_list("id", flat=True))
             if not request.user.is_superuser and request.data["host"] not in hosts:
                 serializer.validated_data["host"] = None
 
@@ -691,9 +678,8 @@ class APINoteViewSet(
         """
         show_pk = get_values(self.kwargs, "show_pk")
 
-        if (
-            not request.user.is_superuser
-            and show_pk not in request.user.shows.values_list("id", flat=True)
+        if not request.user.is_superuser and show_pk not in request.user.shows.values_list(
+            "id", flat=True
         ):
             return Response(status=status.HTTP_401_UNAUTHORIZED)
 
@@ -701,14 +687,10 @@ class APINoteViewSet(
         serializer = NoteSerializer(note, data=request.data)
 
         if serializer.is_valid():
-            hosts = Host.objects.filter(
-                shows__in=request.user.shows.values_list("id", flat=True)
-            )
+            hosts = Host.objects.filter(shows__in=request.user.shows.values_list("id", flat=True))
             # Don't assign a host the user mustn't edit. Reassign the original value instead
             if not request.user.is_superuser and int(request.data["host"]) not in hosts:
-                serializer.validated_data["host"] = Host.objects.filter(
-                    pk=note.host_id
-                )[0]
+                serializer.validated_data["host"] = Host.objects.filter(pk=note.host_id)[0]
 
             serializer.save()
             return Response(serializer.data)
@@ -721,9 +703,8 @@ class APINoteViewSet(
         """
         show_pk = get_values(self.kwargs, "show_pk")
 
-        if (
-            not request.user.is_superuser
-            and show_pk not in request.user.shows.values_list("id", flat=True)
+        if not request.user.is_superuser and show_pk not in request.user.shows.values_list(
+            "id", flat=True
         ):
             return Response(status=status.HTTP_401_UNAUTHORIZED)
 
@@ -792,9 +773,7 @@ class APIMusicFocusViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
     create=extend_schema(summary="Create a new funding category."),
     retrieve=extend_schema(summary="Retrieve a single funding category."),
     update=extend_schema(summary="Update an existing funding category."),
-    partial_update=extend_schema(
-        summary="Partially update an existing funding category."
-    ),
+    partial_update=extend_schema(summary="Partially update an existing funding category."),
     destroy=extend_schema(summary="Delete an existing funding category."),
     list=extend_schema(summary="List all funding categories."),
 )
-- 
GitLab