Skip to content

Commit 5ec6294

Browse files
committed
feat: reject unenrolments if less than 48h to the event occurrence
KK-1436. Configure `KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE` with environment variable and use it to limit unenrolments too late before an occurrence start time. The default value is set to 48h. Use 0 as a value to disable this new feature. The UI changes are implemented in City-of-Helsinki/kukkuu-ui#693.
1 parent 82e108a commit 5ec6294

6 files changed

Lines changed: 76 additions & 4 deletions

File tree

events/schema.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import logging
22
from copy import deepcopy
3+
from datetime import timedelta
34
from typing import List, Optional
45

56
import graphene
67
from auditlog_extra.graphene_decorators import auditlog_access
78
from django.apps import apps
9+
from django.conf import settings
810
from django.core.exceptions import PermissionDenied, ValidationError
911
from django.db import transaction
1012
from django.db.models import Count, Prefetch, Q
13+
from django.utils import timezone
1114
from django.utils.translation import get_language
1215
from graphene import Connection, ObjectType, relay
1316
from graphene_django import DjangoObjectType
@@ -49,6 +52,7 @@
4952
TicketSystemPasswordAlreadyAssignedError,
5053
TicketSystemPasswordNothingToImportError,
5154
TicketVerificationError,
55+
TooLateToUnenrolError,
5256
)
5357
from kukkuu.utils import get_kukkuu_error_by_code
5458
from projects.models import Project
@@ -61,19 +65,33 @@
6165
EventGroupTranslation = apps.get_model("events", "EventGroupTranslation")
6266

6367

64-
def validate_enrolment(child: Child, occurrence: Occurrence):
68+
def validate_enrolment(child: Child, occurrence: Occurrence) -> None:
6569
if reason := occurrence.get_enrolment_denied_reason(child):
6670
raise ENROLMENT_DENIED_REASON_TO_GRAPHQL_ERROR[reason]
6771

6872

69-
def validate_enrolment_deletion(enrolment):
73+
def validate_enrolment_deletion(enrolment: Enrolment) -> None:
74+
now = timezone.now()
75+
occurrence_time = enrolment.occurrence.time
76+
77+
# Check if the enrolment is for an upcoming occurrence.
78+
# User should never be able to unenrol, if the occurrence is in the past.
7079
if not enrolment.is_upcoming():
7180
raise PastEnrolmentError(
7281
"Cannot unenrol from an occurrence that is in the past."
7382
)
7483

84+
# Get the unenrol hours limit from settings, 0 means no time limit
85+
# (only past occurrences are not allowed to unenrol).
86+
hours_before = getattr(settings, "KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE", 48)
87+
if hours_before > 0 and (occurrence_time - now) <= timedelta(hours=hours_before):
88+
raise TooLateToUnenrolError(
89+
"Cannot unenrol from an occurrence that starts within "
90+
f"{hours_before} hours."
91+
)
92+
7593

76-
def validate_occurrence_input(kwargs, occurrence: Occurrence = None):
94+
def validate_occurrence_input(kwargs, occurrence: Occurrence = None) -> None:
7795
if time := kwargs.get("time"):
7896
if occurrence:
7997
# Don't consider the occurrence which is being updated

events/tests/test_api.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,9 @@ def test_unenrol_occurrence(
979979
occurrence,
980980
project,
981981
child_with_random_guardian,
982+
settings,
982983
):
984+
settings.KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE = 0
983985
non_authen_executed = api_client.execute(
984986
UNENROL_OCCURRENCE_MUTATION, variables=ENROL_OCCURRENCE_VARIABLES
985987
)
@@ -1021,7 +1023,8 @@ def test_unenrol_occurrence(
10211023
snapshot.assert_match(executed)
10221024

10231025

1024-
def test_ticket_not_valid_after_unenrol(user_api_client, occurrence, project):
1026+
def test_ticket_not_valid_after_unenrol(user_api_client, occurrence, project, settings):
1027+
settings.KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE = 0
10251028
child = ChildWithGuardianFactory(
10261029
relationship__guardian__user=user_api_client.user, project=project
10271030
)
@@ -1059,6 +1062,44 @@ def test_cannot_unenrol_from_occurrence_in_past(
10591062
assert_match_error_code(executed, PAST_ENROLMENT_ERROR)
10601063

10611064

1065+
@freeze_time("2024-01-01 12:00:00")
1066+
def test_cannot_unenrol_within_48_hours(guardian_api_client, child_with_user_guardian):
1067+
# Create occurrence 47 hours in future (within 48 hour limit)
1068+
future_time = timezone.now() + timedelta(hours=47)
1069+
enrolment = EnrolmentFactory(
1070+
occurrence__time=future_time, child=child_with_user_guardian
1071+
)
1072+
1073+
variables = deepcopy(UNENROL_OCCURRENCE_VARIABLES)
1074+
variables["input"]["occurrenceId"] = get_global_id(enrolment.occurrence)
1075+
variables["input"]["childId"] = get_global_id(child_with_user_guardian)
1076+
1077+
executed = guardian_api_client.execute(
1078+
UNENROL_OCCURRENCE_MUTATION, variables=variables
1079+
)
1080+
1081+
assert_match_error_code(executed, "TOO_LATE_TO_UNENROL_ERROR")
1082+
1083+
1084+
@freeze_time("2024-01-01 12:00:00")
1085+
def test_can_unenrol_outside_48_hours(guardian_api_client, child_with_user_guardian):
1086+
# Create occurrence 49 hours in future (outside 48 hour limit)
1087+
future_time = timezone.now() + timedelta(hours=49)
1088+
enrolment = EnrolmentFactory(
1089+
occurrence__time=future_time, child=child_with_user_guardian
1090+
)
1091+
1092+
variables = deepcopy(UNENROL_OCCURRENCE_VARIABLES)
1093+
variables["input"]["occurrenceId"] = get_global_id(enrolment.occurrence)
1094+
variables["input"]["childId"] = get_global_id(child_with_user_guardian)
1095+
1096+
executed = guardian_api_client.execute(
1097+
UNENROL_OCCURRENCE_MUTATION, variables=variables
1098+
)
1099+
1100+
assert "errors" not in executed
1101+
1102+
10621103
def test_maximum_enrolment(
10631104
guardian_api_client, occurrence, project, child_with_user_guardian
10641105
):

kukkuu/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
OCCURRENCE_IS_NOT_FULL_ERROR = "OCCURRENCE_IS_NOT_FULL_ERROR"
2525
MESSAGE_ALREADY_SENT_ERROR = "MESSAGE_ALREADY_SENT_ERROR"
2626
PAST_ENROLMENT_ERROR = "PAST_ENROLMENT_ERROR"
27+
TOO_LATE_TO_UNENROL_ERROR = "TOO_LATE_TO_UNENROL_ERROR"
2728
SINGLE_EVENTS_DISALLOWED_ERROR = "SINGLE_EVENTS_DISALLOWED_ERROR"
2829
TICKET_SYSTEM_URL_MISSING_ERROR = "TICKET_SYSTEM_URL_MISSING_ERROR"
2930
NO_FREE_TICKET_SYSTEM_PASSWORDS_ERROR = "NO_FREE_TICKET_SYSTEM_PASSWORDS_ERROR"

kukkuu/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class PastEnrolmentError(KukkuuGraphQLError):
9999
"""Cannot unenrol because the enrolment is in the past"""
100100

101101

102+
class TooLateToUnenrolError(KukkuuGraphQLError):
103+
"""Cannot unenrol because the occurrence starts too soon"""
104+
105+
102106
class SingleEventsDisallowedError(KukkuuGraphQLError):
103107
"""Cannot create an event outside event groups"""
104108

kukkuu/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
KUKKUU_REMINDER_DAYS_IN_ADVANCE=(int, 1),
8181
KUKKUU_FEEDBACK_NOTIFICATION_DELAY=(int, 15),
8282
KUKKUU_NOTIFICATIONS_SHEET_ID=(str, ""),
83+
KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE=(int, 48),
8384
LOGIN_REDIRECT_URL=(str, "/admin/"),
8485
LOGOUT_REDIRECT_URL=(str, "/admin/"),
8586
VERIFICATION_TOKEN_VALID_MINUTES=(int, 15),
@@ -471,6 +472,10 @@ def sentry_before_send(event: Event, hint: Hint):
471472
KUKKUU_REMINDER_DAYS_IN_ADVANCE = env("KUKKUU_REMINDER_DAYS_IN_ADVANCE")
472473
KUKKUU_FEEDBACK_NOTIFICATION_DELAY = env("KUKKUU_FEEDBACK_NOTIFICATION_DELAY")
473474
KUKKUU_NOTIFICATIONS_SHEET_ID = env("KUKKUU_NOTIFICATIONS_SHEET_ID")
475+
# How many hours before the occurrence start time
476+
# the user can unenrol from the occurrence.
477+
# If set to 0, unenrolment is allowed till the occurrence start.
478+
KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE = env("KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE")
474479

475480
KUKKUU_HASHID_MIN_LENGTH = 5
476481
KUKKUU_HASHID_ALPHABET = "abcdefghijklmnopqrstuvwxyz"

kukkuu/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
TICKET_SYSTEM_PASSWORD_ALREADY_ASSIGNED_ERROR,
3939
TICKET_SYSTEM_PASSWORD_NOTHING_TO_IMPORT_ERROR,
4040
TICKET_SYSTEM_URL_MISSING_ERROR,
41+
TOO_LATE_TO_UNENROL_ERROR,
4142
VERIFICATION_TOKEN_INVALID_ERROR,
4243
)
4344
from kukkuu.exceptions import (
@@ -67,6 +68,7 @@
6768
TicketSystemPasswordAlreadyAssignedError,
6869
TicketSystemPasswordNothingToImportError,
6970
TicketSystemUrlMissingError,
71+
TooLateToUnenrolError,
7072
VerificationTokenInvalidError,
7173
)
7274

@@ -97,6 +99,7 @@
9799
EventGroupNotReadyForPublishingError: EVENT_GROUP_NOT_READY_FOR_PUBLISHING_ERROR,
98100
EventNotPublishedError: EVENT_NOT_PUBLISHED_ERROR,
99101
PastEnrolmentError: PAST_ENROLMENT_ERROR,
102+
TooLateToUnenrolError: TOO_LATE_TO_UNENROL_ERROR,
100103
SingleEventsDisallowedError: SINGLE_EVENTS_DISALLOWED_ERROR,
101104
TicketSystemUrlMissingError: TICKET_SYSTEM_URL_MISSING_ERROR,
102105
NoFreeTicketSystemPasswordsError: NO_FREE_TICKET_SYSTEM_PASSWORDS_ERROR,

0 commit comments

Comments
 (0)