From 4cee41705c4787005488e808826af65e1d74e970 Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Tue, 10 Dec 2024 18:23:24 +0200 Subject: [PATCH 1/4] Add reservation unit `require_adult_reservee` field --- ..._reservation_unit_require_adult_reservee.py | 18 ++++++++++++++++++ .../models/reservation_unit/model.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 tilavarauspalvelu/migrations/0055_add_reservation_unit_require_adult_reservee.py diff --git a/tilavarauspalvelu/migrations/0055_add_reservation_unit_require_adult_reservee.py b/tilavarauspalvelu/migrations/0055_add_reservation_unit_require_adult_reservee.py new file mode 100644 index 000000000..0b2f6f1f5 --- /dev/null +++ b/tilavarauspalvelu/migrations/0055_add_reservation_unit_require_adult_reservee.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-12-10 16:00 +from __future__ import annotations + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tilavarauspalvelu", "0054_bugreport"), + ] + + operations = [ + migrations.AddField( + model_name="reservationunit", + name="require_adult_reservee", + field=models.BooleanField(blank=True, default=False), + ), + ] diff --git a/tilavarauspalvelu/models/reservation_unit/model.py b/tilavarauspalvelu/models/reservation_unit/model.py index 57342019a..9daacaf36 100644 --- a/tilavarauspalvelu/models/reservation_unit/model.py +++ b/tilavarauspalvelu/models/reservation_unit/model.py @@ -94,6 +94,7 @@ class ReservationUnit(models.Model): is_draft: bool = models.BooleanField(default=False, blank=True, db_index=True) is_archived: bool = models.BooleanField(default=False, db_index=True) require_introduction: bool = models.BooleanField(default=False) + require_adult_reservee: bool = models.BooleanField(default=False, blank=True) require_reservation_handling: bool = models.BooleanField(default=False, blank=True) reservation_block_whole_day: bool = models.BooleanField(default=False, blank=True) can_apply_free_of_charge: bool = models.BooleanField(default=False, blank=True) From 9821d1454d3e9ea41df518f27291dc8066c3e9a6 Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Tue, 10 Dec 2024 18:32:04 +0200 Subject: [PATCH 2/4] Add `require_adult_reservee` to admin panel --- locale/fi/LC_MESSAGES/django.po | 16 +++++++++++++--- locale/sv/LC_MESSAGES/django.po | 14 ++++++++++++-- .../admin/reservation_unit/admin.py | 1 + tilavarauspalvelu/admin/reservation_unit/form.py | 8 ++++++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index 91e9444f4..1f2837f1b 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -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" @@ -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 " diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index 44313a0d9..4b0d1a352 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -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 @@ -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 "" diff --git a/tilavarauspalvelu/admin/reservation_unit/admin.py b/tilavarauspalvelu/admin/reservation_unit/admin.py index d27f6aa91..3b5a41f04 100644 --- a/tilavarauspalvelu/admin/reservation_unit/admin.py +++ b/tilavarauspalvelu/admin/reservation_unit/admin.py @@ -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", diff --git a/tilavarauspalvelu/admin/reservation_unit/form.py b/tilavarauspalvelu/admin/reservation_unit/form.py index 9591ef5de..73bf65ab3 100644 --- a/tilavarauspalvelu/admin/reservation_unit/form.py +++ b/tilavarauspalvelu/admin/reservation_unit/form.py @@ -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"), @@ -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."), From 5fae1551e3f49166b146a2630cb623d565fdd0a2 Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Tue, 10 Dec 2024 19:19:52 +0200 Subject: [PATCH 3/4] Add check for age to reservation create endpoint --- config/settings.py | 1 + tests/factories/reservation_unit.py | 1 + .../test_reservation/test_create.py | 94 ++++++++++++++++++- .../graphql/extensions/validation_errors.py | 1 + .../serializers/_base_save_serializer.py | 22 ++++- .../serializers/create_serializer.py | 3 +- tilavarauspalvelu/models/user/actions.py | 27 +++++- 7 files changed, 144 insertions(+), 5 deletions(-) diff --git a/config/settings.py b/config/settings.py index 8bc3e869e..f1db7bd0a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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) diff --git a/tests/factories/reservation_unit.py b/tests/factories/reservation_unit.py index 097bf342d..faf1cd67c 100644 --- a/tests/factories/reservation_unit.py +++ b/tests/factories/reservation_unit.py @@ -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 diff --git a/tests/test_graphql_api/test_reservation/test_create.py b/tests/test_graphql_api/test_reservation/test_create.py index a94f46b4f..6e428123e 100644 --- a/tests/test_graphql_api/test_reservation/test_create.py +++ b/tests/test_graphql_api/test_reservation/test_create.py @@ -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 ( @@ -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 @@ -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 diff --git a/tilavarauspalvelu/api/graphql/extensions/validation_errors.py b/tilavarauspalvelu/api/graphql/extensions/validation_errors.py index 8791da42e..56f5845da 100644 --- a/tilavarauspalvelu/api/graphql/extensions/validation_errors.py +++ b/tilavarauspalvelu/api/graphql/extensions/validation_errors.py @@ -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 diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/_base_save_serializer.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/_base_save_serializer.py index c25022a86..b22ac3946 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/_base_save_serializer.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/_base_save_serializer.py @@ -23,6 +23,7 @@ from utils.date_utils import DEFAULT_TIMEZONE if TYPE_CHECKING: + from tilavarauspalvelu.models import User from tilavarauspalvelu.typing import AnyUser @@ -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) @@ -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) @@ -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) diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializer.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializer.py index 5dc18b938..670eeb1db 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializer.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializer.py @@ -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) diff --git a/tilavarauspalvelu/models/user/actions.py b/tilavarauspalvelu/models/user/actions.py index 033ec2fe8..251b89b8a 100644 --- a/tilavarauspalvelu/models/user/actions.py +++ b/tilavarauspalvelu/models/user/actions.py @@ -5,6 +5,7 @@ from auditlog.models import LogEntry from dateutil.relativedelta import relativedelta +from django.conf import settings from social_django.models import UserSocialAuth from tilavarauspalvelu.enums import ( @@ -25,7 +26,7 @@ UnitRole, ) from tilavarauspalvelu.typing import UserAnonymizationInfo -from utils.date_utils import local_datetime +from utils.date_utils import local_date, local_datetime if TYPE_CHECKING: from collections.abc import Iterable @@ -187,3 +188,27 @@ def can_anonymize(self) -> UserAnonymizationInfo: has_open_applications=has_open_applications, has_open_payments=has_open_payments, ) + + @property + def is_ad_user(self) -> bool: + id_token = self.user.id_token + return id_token is not None and id_token.is_ad_login + + @property + def is_profile_user(self) -> bool: + id_token = self.user.id_token + return id_token is not None and id_token.is_profile_login + + @property + def user_age(self) -> int | None: + birthday = self.user.date_of_birth + if birthday is None: + return None + return relativedelta(local_date(), birthday).years + + @property + def is_of_age(self) -> bool: + user_age = self.user_age + if user_age is None: + return False + return user_age >= settings.USER_IS_ADULT_AT_AGE From f5db0e65168d8e88698745a25640db0f48267fb3 Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Wed, 11 Dec 2024 10:08:11 +0200 Subject: [PATCH 4/4] Add check for age to application create endpoint --- .../test_application/test_create.py | 20 ++++++++++++++++--- .../test_create_permissions.py | 4 +++- .../api/graphql/extensions/error_codes.py | 2 ++ .../graphql/types/application/serializers.py | 8 ++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tests/test_graphql_api/test_application/test_create.py b/tests/test_graphql_api/test_application/test_create.py index fae57b3c2..4da0fd6cb 100644 --- a/tests/test_graphql_api/test_application/test_create.py +++ b/tests/test_graphql_api/test_application/test_create.py @@ -1,5 +1,6 @@ from __future__ import annotations +import freezegun import pytest from tilavarauspalvelu.models import ( @@ -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 @@ -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 @@ -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 @@ -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", @@ -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"] diff --git a/tests/test_graphql_api/test_application/test_create_permissions.py b/tests/test_graphql_api/test_application/test_create_permissions.py index 2f89433c0..d29ec35e4 100644 --- a/tests/test_graphql_api/test_application/test_create_permissions.py +++ b/tests/test_graphql_api/test_application/test_create_permissions.py @@ -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 @@ -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 diff --git a/tilavarauspalvelu/api/graphql/extensions/error_codes.py b/tilavarauspalvelu/api/graphql/extensions/error_codes.py index 09d3bf102..7a4c5ecb3 100644 --- a/tilavarauspalvelu/api/graphql/extensions/error_codes.py +++ b/tilavarauspalvelu/api/graphql/extensions/error_codes.py @@ -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" diff --git a/tilavarauspalvelu/api/graphql/types/application/serializers.py b/tilavarauspalvelu/api/graphql/types/application/serializers.py index 699b8a046..17fc6a3d3 100644 --- a/tilavarauspalvelu/api/graphql/types/application/serializers.py +++ b/tilavarauspalvelu/api/graphql/types/application/serializers.py @@ -22,6 +22,7 @@ from utils.fields.serializer import CurrentUserDefaultNullable if TYPE_CHECKING: + from tilavarauspalvelu.models import User from tilavarauspalvelu.typing import AnyUser @@ -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