Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "require adult reservee" checks #1462

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ def CACHES(cls):
REMOVE_RECURRING_RESERVATIONS_OLDER_THAN_DAYS = 1
REMOVE_EXPIRED_APPLICATIONS_OLDER_THAN_DAYS = 365
TEXT_SEARCH_CACHE_TIME_DAYS = 30
USER_IS_ADULT_AT_AGE = 18

APPLICATION_ROUND_RESERVATION_CREATION_TIMEOUT_MINUTES = values.IntegerValue(default=10)
AFFECTING_TIME_SPANS_UPDATE_INTERVAL_MINUTES = values.IntegerValue(default=2)
Expand Down
16 changes: 13 additions & 3 deletions locale/fi/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -1768,8 +1768,12 @@ msgid "Max reservations per user"
msgstr "Maksimi varauksia per käyttäjä"

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid "Does the reservations of this require a handling"
msgstr "Vaativatko tämän varausyksikön varaukset käsittelyn"
msgid "Require adult reservee"
msgstr "Vaaditaan täysi-ikäinen varaaja"

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid "Require reservation handling"
msgstr "Vaaditaan varausten käsittely"

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid "Authentication"
Expand Down Expand Up @@ -1951,7 +1955,13 @@ msgstr "Maksimi määrä aktiivisia varauksia per käyttäjä"

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid ""
"Does reservations of this reservation unit need to be handled before they're "
"Do reservations to this reservation unit require the reservee to be a legal "
"adult?"
msgstr "Vaativatko tämän varausksikön varaukset varaajan olevan täysi-ikäinen?"

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid ""
"Do reservations to this reservation unit need to be handled before they're "
"confirmed."
msgstr ""
"Vaativatko tämän varausyksikön varaukset käsittelyn ennen kuin ne voidaan "
Expand Down
14 changes: 12 additions & 2 deletions locale/sv/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -1720,7 +1720,11 @@ msgid "Max reservations per user"
msgstr ""

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid "Does the reservations of this require a handling"
msgid "Require adult reservee"
msgstr ""

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid "Require reservation handling"
msgstr ""

#: tilavarauspalvelu/admin/reservation_unit/form.py
Expand Down Expand Up @@ -1892,7 +1896,13 @@ msgstr ""

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid ""
"Does reservations of this reservation unit need to be handled before they're "
"Do reservations to this reservation unit require the reservee to be a legal "
"adult?"
msgstr ""

#: tilavarauspalvelu/admin/reservation_unit/form.py
msgid ""
"Do reservations to this reservation unit need to be handled before they're "
"confirmed."
msgstr ""

Expand Down
1 change: 1 addition & 0 deletions tests/factories/reservation_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class Meta:
is_draft = False
is_archived = False
require_introduction = False
require_adult_reservee = False
require_reservation_handling = False
reservation_block_whole_day = False
can_apply_free_of_charge = False
Expand Down
20 changes: 17 additions & 3 deletions tests/test_graphql_api/test_application/test_create.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import freezegun
import pytest

from tilavarauspalvelu.models import (
Expand All @@ -11,6 +12,7 @@
ReservationUnitOption,
SuitableTimeRange,
)
from utils.date_utils import local_datetime

from tests.factories import ApplicationRoundFactory
from tests.test_graphql_api.test_application.helpers import get_application_create_data
Expand All @@ -28,7 +30,7 @@ def test_application__create(graphql):
# - There is an open application round
# - A superuser is using the system
application_round = ApplicationRoundFactory.create_in_status_open()
graphql.login_with_superuser()
graphql.login_with_superuser(date_of_birth=local_datetime(2006, 1, 1))

# when:
# - User tries to create a new application without sections
Expand All @@ -55,7 +57,7 @@ def test_application__create__with_application_sections(graphql):
# - There is an open application round
# - A superuser is using the system
application_round = ApplicationRoundFactory.create_in_status_open()
graphql.login_with_superuser()
graphql.login_with_superuser(date_of_birth=local_datetime(2006, 1, 1))

assert Application.objects.count() == 0

Expand Down Expand Up @@ -90,7 +92,7 @@ def test_application__create__sub_serializer_error(graphql, field):
# - There is an open application round
# - A superuser is using the system
application_round = ApplicationRoundFactory.create_in_status_open()
graphql.login_with_superuser()
graphql.login_with_superuser(date_of_birth=local_datetime(2006, 1, 1))

address_data = {
"streetAddress": "Address",
Expand All @@ -116,3 +118,15 @@ def test_application__create__sub_serializer_error(graphql, field):
"message": "This field may not be blank.",
}
]


@freezegun.freeze_time(local_datetime(2024, 1, 1))
def test_application__create__is_under_age(graphql):
application_round = ApplicationRoundFactory.create_in_status_open()
graphql.login_with_superuser(date_of_birth=local_datetime(2006, 1, 2))

input_data = get_application_create_data(application_round)
response = graphql(CREATE_MUTATION, input_data=input_data)

assert response.error_message() == "Mutation was unsuccessful."
assert response.field_error_messages("user") == ["Application can only be created by an adult reservee"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import pytest

from utils.date_utils import local_datetime

from tests.factories import ApplicationRoundFactory

from .helpers import CREATE_MUTATION, get_application_create_data
Expand Down Expand Up @@ -33,7 +35,7 @@ def test_regular_user_can_create_application(graphql):
# - There is an open application round
# - A regular user is using the system
application_round = ApplicationRoundFactory.create_in_status_open()
graphql.login_with_regular_user()
graphql.login_with_regular_user(date_of_birth=local_datetime(2006, 1, 1))

# when:
# - User tries to create a new application
Expand Down
94 changes: 93 additions & 1 deletion tests/test_graphql_api/test_reservation/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import freezegun
import pytest
from freezegun import freeze_time
from graphene_django_extensions.testing import parametrize_helper

from tilavarauspalvelu.enums import (
Expand All @@ -18,7 +19,14 @@
from tilavarauspalvelu.models import Reservation, ReservationUnitHierarchy
from tilavarauspalvelu.utils.helauth.clients import HelsinkiProfileClient
from tilavarauspalvelu.utils.helauth.typing import ADLoginAMR
from utils.date_utils import DEFAULT_TIMEZONE, local_datetime, local_end_of_day, local_start_of_day, next_hour
from utils.date_utils import (
DEFAULT_TIMEZONE,
local_date,
local_datetime,
local_end_of_day,
local_start_of_day,
next_hour,
)
from utils.decimal_utils import round_decimal
from utils.sentry import SentryLogger

Expand Down Expand Up @@ -1125,3 +1133,87 @@ def test_reservation__create__reservee_used_ad_login(graphql, amr, expected):

reservation = Reservation.objects.get(pk=response.first_query_object["pk"])
assert reservation.reservee_used_ad_login is expected


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_adult(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

user = UserFactory.create(social_auth__extra_data__amr="suomi_fi", date_of_birth=local_date(2006, 1, 1))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_under_age(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

user = UserFactory.create(social_auth__extra_data__amr="suomi_fi", date_of_birth=local_date(2006, 1, 2))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.error_message() == "Reservation unit can only be booked by an adult reservee"


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_under_age__reservation_unit_allows(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=False)

user = UserFactory.create(social_auth__extra_data__amr="suomi_fi", date_of_birth=local_date(2006, 1, 2))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__is_ad_user(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

user = UserFactory.create(social_auth__extra_data__amr="helsinkiazuread", date_of_birth=local_date(2006, 1, 1))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None


@freeze_time(local_datetime(2024, 1, 1))
def test_reservation__create__require_adult_reservee__no_id_token(graphql):
reservation_unit = ReservationUnitFactory.create_reservable_now(require_adult_reservee=True)

# We don't have an ID token, so we don't know if this is an AD user.
# Still, we have have a birthday that indicates they are an adult.
user = UserFactory.create(date_of_birth=local_date(2006, 1, 1))

graphql.force_login(user)

data = get_create_data(reservation_unit)
response = graphql(CREATE_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

reservation = Reservation.objects.filter(pk=response.first_query_object["pk"]).first()
assert reservation is not None
1 change: 1 addition & 0 deletions tilavarauspalvelu/admin/reservation_unit/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class ReservationUnitAdmin(SortableAdminMixin, TabbedTranslationAdmin):
"origin_hauki_resource",
"allow_reservations_without_opening_hours",
"require_introduction",
"require_adult_reservee",
"require_reservation_handling",
"reservation_block_whole_day",
"is_draft",
Expand Down
8 changes: 6 additions & 2 deletions tilavarauspalvelu/admin/reservation_unit/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ class Meta:
"publish_ends": _("Publish ends"),
"metadata_set": _("Reservation metadata set"),
"max_reservations_per_user": _("Max reservations per user"),
"require_reservation_handling": _("Does the reservations of this require a handling"),
"require_adult_reservee": _("Require adult reservee"),
"require_reservation_handling": _("Require reservation handling"),
"authentication": _("Authentication"),
"reservation_kind": _("Reservation kind"),
"payment_types": _("Payment types"),
Expand Down Expand Up @@ -176,8 +177,11 @@ class Meta:
"and required form fields for this reservation unit."
),
"max_reservations_per_user": _("Maximum number of active reservations per user"),
"require_adult_reservee": _(
"Do reservations to this reservation unit require the reservee to be a legal adult?",
),
"require_reservation_handling": _(
"Does reservations of this reservation unit need to be handled before they're confirmed."
"Do reservations to this reservation unit need to be handled before they're confirmed."
),
"authentication": _("Authentication required for reserving this reservation unit."),
"reservation_kind": _("What kind of reservations are to be booked with this reservation unit."),
Expand Down
2 changes: 2 additions & 0 deletions tilavarauspalvelu/api/graphql/extensions/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,7 @@
APPLICATION_ROUND_NOT_IN_ALLOCATION = "APPLICATION_ROUND_NOT_IN_ALLOCATION"
APPLICATION_ROUND_NOT_IN_RESULTS_SENT_STATE = "APPLICATION_ROUND_NOT_IN_RESULTS_SENT_STATE"

APPLICATION_ADULT_RESERVEE_REQUIRED = "APPLICATION_ADULT_RESERVEE_REQUIRED"

CANCEL_REASON_DOES_NOT_EXIST = "CANCEL_REASON_DOES_NOT_EXIST"
DENY_REASON_DOES_NOT_EXIST = "DENY_REASON_DOES_NOT_EXIST"
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ValidationErrorCodes(Enum):
INVALID_WEEKDAY = "INVALID_WEEKDAY"
INVALID_RECURRENCE_IN_DAY = "INVALID_RECURRENCE_IN_DAYS"
RESERVATION_TYPE_NOT_ALLOWED = "RESERVATION_TYPE_NOT_ALLOWED"
RESERVATION_ADULT_RESERVEE_REQUIRED = "ADULT_RESERVEE_REQUIRED"


class ValidationErrorWithCode(GraphQLError): # noqa: N818
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from utils.fields.serializer import CurrentUserDefaultNullable

if TYPE_CHECKING:
from tilavarauspalvelu.models import User
from tilavarauspalvelu.typing import AnyUser


Expand Down Expand Up @@ -61,6 +62,13 @@ class Meta:
},
}

def validate_user(self, user: User) -> User:
if user.actions.is_ad_user or user.actions.is_of_age:
return user

msg = "Application can only be created by an adult reservee"
raise ValidationError(msg, error_codes.APPLICATION_ADULT_RESERVEE_REQUIRED)


class ApplicationUpdateSerializer(ApplicationCreateSerializer):
instance: Application
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from utils.date_utils import DEFAULT_TIMEZONE

if TYPE_CHECKING:
from tilavarauspalvelu.models import User
from tilavarauspalvelu.typing import AnyUser


Expand Down Expand Up @@ -142,10 +143,12 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]:
begin = begin.astimezone(DEFAULT_TIMEZONE)
end = end.astimezone(DEFAULT_TIMEZONE)

request_user: AnyUser = self.context["request"].user
reservation_units = self._get_reservation_units(data)

sku = None
for reservation_unit in reservation_units:
self.check_if_reservee_should_be_adult(reservation_unit, request_user)
self.check_reservation_time(reservation_unit)
self.check_reservation_overlap(reservation_unit, begin, end)
self.check_reservation_duration(reservation_unit, begin, end)
Expand All @@ -164,7 +167,6 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]:
data["state"] = ReservationStateChoice.CREATED.value
data["buffer_time_before"], data["buffer_time_after"] = self._calculate_buffers(begin, end, reservation_units)

request_user: AnyUser = self.context["request"].user
data["user"] = None if request_user.is_anonymous else request_user
data["reservee_used_ad_login"] = (
False if request_user.is_anonymous else getattr(request_user.id_token, "is_ad_login", False)
Expand Down Expand Up @@ -220,3 +222,21 @@ def check_reservation_kind(self, reservation_unit: ReservationUnit) -> None:
if reservation_unit.reservation_kind == ReservationKind.SEASON:
msg = "Reservation unit is only available or seasonal booking."
raise ValidationErrorWithCode(msg, ValidationErrorCodes.RESERVATION_UNIT_TYPE_IS_SEASON)

def check_if_reservee_should_be_adult(self, reservation_unit: ReservationUnit, user: User) -> None:
if self.instance is not None:
# Only check for creation
return

if not reservation_unit.require_adult_reservee:
return

# AD users are currently never under age since we have blocked students from signing in.
if user.actions.is_ad_user:
return

if user.actions.is_of_age:
return

msg = "Reservation unit can only be booked by an adult reservee"
raise ValidationErrorWithCode(msg, ValidationErrorCodes.RESERVATION_ADULT_RESERVEE_REQUIRED)
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
from utils.sentry import SentryLogger

if TYPE_CHECKING:
from tilavarauspalvelu.models import Reservation
from tilavarauspalvelu.typing import AnyUser, WSGIRequest


class ReservationCreateSerializer(ReservationBaseSaveSerializer):
instance: Reservation
instance: None

def validate(self, data: dict[str, Any]) -> dict[str, Any]:
data = super().validate(data)
Expand Down
Loading
Loading