Repository: ephios-dev/ephios

Issue:
Shifts back to back are marked as conflicting
As a user, I want to be able to sign up to two shifts that are back to back (e.g. one shift ends at 12:00, the other starts at 12:00). Currently, these are marked as conflicting, so I am not able to sign up.
Shifts back to back are marked as conflicting
As a user, I want to be able to sign up to two shifts that are back to back (e.g. one shift ends at 12:00, the other starts at 12:00). Currently, these are marked as conflicting, so I am not able to sign up.


Discussion:

Resolving patch:
diff --git a/ephios/core/signup/methods.py b/ephios/core/signup/methods.py
--- a/ephios/core/signup/methods.py
+++ b/ephios/core/signup/methods.py
@@ -256,20 +256,17 @@ def check_participant_age(method, participant):
         )
 
 
-def get_conflicting_shifts(shift, participant):
+def get_conflicting_participations(shift, participant):
     return participant.all_participations().filter(
         ~Q(shift=shift)
         & Q(state=AbstractParticipation.States.CONFIRMED)
-        & (
-            Q(shift__start_time__lte=shift.start_time, shift__end_time__gte=shift.start_time)
-            | Q(shift__start_time__gte=shift.start_time, shift__start_time__lt=shift.end_time)
-        )
+        & Q(shift__start_time__lt=shift.end_time, shift__end_time__gt=shift.start_time)
     )
 
 
 def check_conflicting_shifts(method, participant):
-    if get_conflicting_shifts(method.shift, participant).exists():
-        return ParticipationError(_("You are already participating at another shift at this time."))
+    if get_conflicting_participations(method.shift, participant).exists():
+        return ParticipationError(_("You are already confirmed for another shift at this time."))
 
 
 class BaseSignupMethod:
diff --git a/ephios/core/templatetags/user_extras.py b/ephios/core/templatetags/user_extras.py
--- a/ephios/core/templatetags/user_extras.py
+++ b/ephios/core/templatetags/user_extras.py
@@ -2,7 +2,7 @@
 from dynamic_preferences.registries import global_preferences_registry
 
 from ephios.core.consequences import editable_consequences
-from ephios.core.signup import get_conflicting_shifts
+from ephios.core.signup import get_conflicting_participations
 
 register = template.Library()
 
@@ -41,4 +41,6 @@ def get_relevant_qualifications(qualification_queryset):
 
 @register.filter(name="conflicting_shifts")
 def participant_conflicting_shifts(participant, shift):
-    return get_conflicting_shifts(shift, participant).values_list("shift__event__title", flat=True)
+    return get_conflicting_participations(shift, participant).values_list(
+        "shift__event__title", flat=True
+    )


Test patch:
diff --git a/tests/core/test_signup_methods.py b/tests/core/test_signup_methods.py
--- a/tests/core/test_signup_methods.py
+++ b/tests/core/test_signup_methods.py
@@ -1,6 +1,10 @@
+from datetime import datetime, timedelta
+
 import pytest
 
-from ephios.core.signup import SignupStats
+from ephios.core.models import AbstractParticipation, Shift
+from ephios.core.signup import LocalParticipation, SignupStats, get_conflicting_participations
+from ephios.plugins.basesignup.signup.instant import InstantConfirmationSignupMethod
 
 
 @pytest.mark.django_db
@@ -13,7 +17,64 @@ def test_signup_stats_addition(django_app):
 
 
 @pytest.mark.django_db
-def test_signup_conflicting_shifts(django_app, volunteer, event, conflicting_event):
+def test_cannot_sign_up_for_conflicting_shifts(django_app, volunteer, event, conflicting_event):
     assert not conflicting_event.shifts.first().signup_method.can_sign_up(
         volunteer.as_participant()
     )
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+    "a_times,b_times,conflict_expected",
+    [
+        (
+            (datetime(2099, 1, 1, 8), datetime(2099, 1, 1, 12)),
+            (datetime(2099, 1, 1, 6), datetime(2099, 1, 1, 8)),
+            False,
+        ),
+        (
+            (datetime(2099, 1, 1, 8), datetime(2099, 1, 1, 12)),
+            (datetime(2099, 1, 1, 6), datetime(2099, 1, 1, 10)),
+            True,
+        ),
+        (
+            (datetime(2099, 1, 1, 8), datetime(2099, 1, 1, 12)),
+            (datetime(2099, 1, 1, 6), datetime(2099, 1, 1, 12)),
+            True,
+        ),
+        (
+            (datetime(2099, 1, 1, 8), datetime(2099, 1, 1, 12)),
+            (datetime(2099, 1, 1, 10), datetime(2099, 1, 1, 18)),
+            True,
+        ),
+        (
+            (datetime(2099, 1, 1, 8), datetime(2099, 1, 1, 12)),
+            (datetime(2099, 1, 1, 11, 59), datetime(2099, 1, 1, 12)),
+            True,
+        ),
+        (
+            (datetime(2099, 1, 1, 8), datetime(2099, 1, 1, 12)),
+            (datetime(2099, 1, 1, 12), datetime(2099, 1, 1, 18)),
+            False,
+        ),
+    ],
+)
+def test_get_conflicting_shifts(a_times, b_times, conflict_expected, event, volunteer):
+    common = dict(signup_method_slug=InstantConfirmationSignupMethod.slug, event=event)
+    a = Shift.objects.create(
+        start_time=a_times[0],
+        end_time=a_times[1],
+        meeting_time=a_times[0] - timedelta(minutes=15),
+        **common
+    )
+    b = Shift.objects.create(
+        start_time=b_times[0],
+        end_time=b_times[1],
+        meeting_time=b_times[0] - timedelta(minutes=15),
+        **common
+    )
+    a_participation = LocalParticipation.objects.create(
+        shift=a, user=volunteer, state=AbstractParticipation.States.CONFIRMED
+    )
+    expected = {a_participation} if conflict_expected else set()
+    assert set(get_conflicting_participations(b, volunteer.as_participant())) == expected