diff --git a/actions/recurring_reservation.py b/actions/recurring_reservation.py index 562d2d0af..85477efa2 100644 --- a/actions/recurring_reservation.py +++ b/actions/recurring_reservation.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any, TypedDict from common.date_utils import DEFAULT_TIMEZONE, combine, get_periods_between -from opening_hours.utils.time_span_element import TimeSpanElement from reservations.enums import RejectionReadinessChoice, ReservationTypeChoice, ReservationTypeStaffChoice from reservations.models import ( AffectingTimeSpan, @@ -15,6 +14,7 @@ Reservation, ReservationPurpose, ) +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement if TYPE_CHECKING: from collections.abc import Collection, Iterable @@ -22,9 +22,8 @@ from django.db import models from applications.models import City - from opening_hours.models import ReservableTimeSpan from reservations.enums import CustomerTypeChoice, ReservationStateChoice - from tilavarauspalvelu.models import User + from tilavarauspalvelu.models import ReservableTimeSpan, User class ReservationPeriod(TypedDict): diff --git a/actions/reservation_unit.py b/actions/reservation_unit.py index e833dec51..54c928a45 100644 --- a/actions/reservation_unit.py +++ b/actions/reservation_unit.py @@ -4,11 +4,11 @@ from typing import TYPE_CHECKING, Any from common.date_utils import DEFAULT_TIMEZONE, time_as_timedelta -from opening_hours.errors import HaukiAPIError -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_api_types import HaukiAPIResource, HaukiTranslatedField from reservation_units.enums import ReservationStartInterval +from tilavarauspalvelu.exceptions import HaukiAPIError +from tilavarauspalvelu.models import OriginHaukiResource +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIResource, HaukiTranslatedField from utils.external_service.errors import ExternalServiceError if TYPE_CHECKING: @@ -18,7 +18,7 @@ from reservation_units.models import ReservationUnit from reservations.models import Reservation - from tilavarauspalvelu.models import Building, Location + from tilavarauspalvelu.models import Building, Location, ReservableTimeSpan __all__ = [ "ReservationUnitActions", diff --git a/applications/tasks.py b/applications/tasks.py index 1b9c7d08e..5fd738f3a 100644 --- a/applications/tasks.py +++ b/applications/tasks.py @@ -10,12 +10,12 @@ from common.date_utils import local_end_of_day, local_start_of_day from common.utils import translate_for_user from config.celery import app -from opening_hours.enums import HaukiResourceState -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.time_span_element import TimeSpanElement from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from reservations.models import RecurringReservation from reservations.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task +from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement from utils.sentry import SentryLogger diff --git a/common/date_utils.py b/common/date_utils.py index b28ec6bc4..17dc0feca 100644 --- a/common/date_utils.py +++ b/common/date_utils.py @@ -465,3 +465,10 @@ def get_periods_between( for delta in range(0, (end_date - start_date).days + 1, interval): yield start_datetime + datetime.timedelta(days=delta), end_datetime + datetime.timedelta(days=delta) + + +def normalize_as_datetime(value: datetime.date | datetime.datetime, *, timedelta_days: int = 0) -> datetime.datetime: + if isinstance(value, datetime.datetime): + return value + # Convert dates to datetimes to include timezone information + return combine(value, datetime.time.min, tzinfo=DEFAULT_TIMEZONE) + datetime.timedelta(days=timedelta_days) diff --git a/common/management/commands/data_creation/create_caisa.py b/common/management/commands/data_creation/create_caisa.py index a8a22ca07..52fbed79a 100644 --- a/common/management/commands/data_creation/create_caisa.py +++ b/common/management/commands/data_creation/create_caisa.py @@ -1,6 +1,5 @@ from datetime import date, timedelta -from opening_hours.models import OriginHaukiResource from reservation_units.enums import ( AuthenticationType, PriceUnit, @@ -22,7 +21,15 @@ ) from reservations.models import ReservationMetadataSet from tilavarauspalvelu.enums import TermsOfUseTypeChoices -from tilavarauspalvelu.models import PaymentAccounting, PaymentMerchant, PaymentProduct, Space, TermsOfUse, Unit +from tilavarauspalvelu.models import ( + OriginHaukiResource, + PaymentAccounting, + PaymentMerchant, + PaymentProduct, + Space, + TermsOfUse, + Unit, +) from .utils import SetName, with_logs diff --git a/common/management/commands/data_creation/create_misc.py b/common/management/commands/data_creation/create_misc.py index 4ced750a4..243a2a434 100644 --- a/common/management/commands/data_creation/create_misc.py +++ b/common/management/commands/data_creation/create_misc.py @@ -5,9 +5,9 @@ from django.utils.timezone import localtime from django_celery_beat.models import CrontabSchedule, PeriodicTask +from common.date_utils import DEFAULT_TIMEZONE from common.enums import BannerNotificationLevel, BannerNotificationTarget from common.models import BannerNotification -from opening_hours.models import DEFAULT_TIMEZONE from reservations.tasks import prune_reservations_task, update_affecting_time_spans_task, update_expired_orders_task from .utils import faker_en, faker_fi, faker_sv, with_logs diff --git a/common/management/commands/data_creation/create_reservation_units.py b/common/management/commands/data_creation/create_reservation_units.py index 78412fe30..190abce37 100644 --- a/common/management/commands/data_creation/create_reservation_units.py +++ b/common/management/commands/data_creation/create_reservation_units.py @@ -7,7 +7,6 @@ from decimal import Decimal from itertools import cycle -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan from reservation_units.enums import ( AuthenticationType, PriceUnit, @@ -29,7 +28,7 @@ ) from reservations.models import ReservationMetadataSet from tilavarauspalvelu.enums import TermsOfUseTypeChoices -from tilavarauspalvelu.models import Resource, Service, TermsOfUse, Unit +from tilavarauspalvelu.models import OriginHaukiResource, ReservableTimeSpan, Resource, Service, TermsOfUse, Unit from .create_seasonal_booking import _create_application_round_time_slots from .utils import ( diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index 4765de88c..3d22bd2e4 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -1346,106 +1346,6 @@ msgstr "Englanti" msgid "Swedish" msgstr "Ruotsi" -#: opening_hours/admin/origin_hauki_resource.py -msgid "" -"OriginHaukiResources with this specific hash never have any opening hours." -msgstr "" -"OriginHaukiResources, joilla on tämä tietty tarkiste, eivät koskaan sisällä " -"aukioloaikoja." - -#: opening_hours/admin/origin_hauki_resource.py -msgid "ID of the resource in Hauki." -msgstr "Resurssin tunniste Aukiolosovelluksesta." - -#: opening_hours/admin/origin_hauki_resource.py -msgid "" -"Hash of the opening hours. Used to determine if the opening hours have " -"changed." -msgstr "" -"Aukioloaikojen tarkiste. Käytetään määrittämään, ovatko aukioloajat " -"muuttuneet." - -#: opening_hours/admin/origin_hauki_resource.py -msgid "" -"All opening hours have been fetched from Hauki up until this date. Opening " -"hours are fetched until the last day of the month two years from now." -msgstr "" -"Kaikki aukioloajat on haettu Aukiolosovelluksesta tähän päivämäärään asti. " -"Aukioloajat haetaan kaksi vuotta eteenpäin." - -#: opening_hours/admin/origin_hauki_resource.py -msgid "Reservable Time Spans updated." -msgstr "Varattavat aikavälit päivitetty." - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Open" -msgstr "Auki" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Closed" -msgstr "Suljettu" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Undefined" -msgstr "Määrittelemätön" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Self service" -msgstr "Itsepalvelu" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "With key" -msgstr "Avaimella" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "With reservation" -msgstr "Varauksella" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Open and reservable" -msgstr "Auki ja varattavissa" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "With key and reservation" -msgstr "Avaimella ja varauksella" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Enter only" -msgstr "Vain sisäänkäynti" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Exit only" -msgstr "Vain uloskäynti" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Weather permitting" -msgstr "Sään salliessa" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Not in use" -msgstr "Ei vuoroa käytössä" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Maintenance" -msgstr "Siivoustauko/huoltotauko" - -#: opening_hours/models.py -msgid "`start_datetime` must be before `end_datetime`." -msgstr "`start_datetime` täytyy olla ennen `end_datetime`." - #: reservation_units/admin/reservation_unit/admin.py msgid "Search by ID, name, unit name, or service sector name" msgstr "Etsi ID:llä, nimellä, toimipisteen nimellä tai palvelualueen nimellä" @@ -2804,6 +2704,37 @@ msgstr "Testisähköposti '%s' lähetetty onnistuneesti." msgid "Search by username, email, first name or last name" msgstr "Etsi käyttäjänimellä, sähköpostilla, etu- tai sukunimellä" +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"OriginHaukiResources with this specific hash never have any opening hours." +msgstr "" +"OriginHaukiResources, joilla on tämä tietty tarkiste, eivät koskaan sisällä " +"aukioloaikoja." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "ID of the resource in Hauki." +msgstr "Resurssin tunniste Aukiolosovelluksesta." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"Hash of the opening hours. Used to determine if the opening hours have " +"changed." +msgstr "" +"Aukioloaikojen tarkiste. Käytetään määrittämään, ovatko aukioloajat " +"muuttuneet." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"All opening hours have been fetched from Hauki up until this date. Opening " +"hours are fetched until the last day of the month two years from now." +msgstr "" +"Kaikki aukioloajat on haettu Aukiolosovelluksesta tähän päivämäärään asti. " +"Aukioloajat haetaan kaksi vuotta eteenpäin." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "Reservable Time Spans updated." +msgstr "Varattavat aikavälit päivitetty." + #: tilavarauspalvelu/admin/payment_merchant/admin.py msgid "The Paytrail Merchant ID should be a six-digit number." msgstr "Kaupan paytrail-tunnuksen tulisi olla kuusinumeroinen." @@ -3061,6 +2992,71 @@ msgstr "Varaaja" msgid "Notification manager" msgstr "Ilmoituksen hallitsija." +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Open" +msgstr "Auki" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Closed" +msgstr "Suljettu" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Undefined" +msgstr "Määrittelemätön" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Self service" +msgstr "Itsepalvelu" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "With key" +msgstr "Avaimella" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "With reservation" +msgstr "Varauksella" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Open and reservable" +msgstr "Auki ja varattavissa" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "With key and reservation" +msgstr "Avaimella ja varauksella" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Enter only" +msgstr "Vain sisäänkäynti" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Exit only" +msgstr "Vain uloskäynti" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Weather permitting" +msgstr "Sään salliessa" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Not in use" +msgstr "Ei vuoroa käytössä" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Maintenance" +msgstr "Siivoustauko/huoltotauko" + #: tilavarauspalvelu/models/email_template/model.py msgid "Email type" msgstr "Sähköpostin tyyppi" @@ -3125,6 +3121,10 @@ msgstr "Täytyy olla suurempi kuin 0" msgid "Must be the sum of net and vat amounts" msgstr "Täytyy olla netto- ja ALV-määrien summa" +#: tilavarauspalvelu/models/reservable_time_span/model.py +msgid "`start_datetime` must be before `end_datetime`." +msgstr "`start_datetime` täytyy olla ennen `end_datetime`." + #: tilavarauspalvelu/models/terms_of_use/model.py msgctxt "singular" msgid "terms of use" diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index 5687731ff..efe62559b 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -1306,100 +1306,6 @@ msgstr "" msgid "Swedish" msgstr "" -#: opening_hours/admin/origin_hauki_resource.py -msgid "" -"OriginHaukiResources with this specific hash never have any opening hours." -msgstr "" - -#: opening_hours/admin/origin_hauki_resource.py -msgid "ID of the resource in Hauki." -msgstr "" - -#: opening_hours/admin/origin_hauki_resource.py -msgid "" -"Hash of the opening hours. Used to determine if the opening hours have " -"changed." -msgstr "" - -#: opening_hours/admin/origin_hauki_resource.py -msgid "" -"All opening hours have been fetched from Hauki up until this date. Opening " -"hours are fetched until the last day of the month two years from now." -msgstr "" - -#: opening_hours/admin/origin_hauki_resource.py -msgid "Reservable Time Spans updated." -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Open" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Closed" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Undefined" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Self service" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "With key" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "With reservation" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Open and reservable" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "With key and reservation" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Enter only" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Exit only" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Weather permitting" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Not in use" -msgstr "" - -#: opening_hours/enums.py -msgctxt "HaukiResourceState" -msgid "Maintenance" -msgstr "" - -#: opening_hours/models.py -msgid "`start_datetime` must be before `end_datetime`." -msgstr "" - #: reservation_units/admin/reservation_unit/admin.py msgid "Search by ID, name, unit name, or service sector name" msgstr "" @@ -2772,6 +2678,31 @@ msgstr "" msgid "Search by username, email, first name or last name" msgstr "" +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"OriginHaukiResources with this specific hash never have any opening hours." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "ID of the resource in Hauki." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"Hash of the opening hours. Used to determine if the opening hours have " +"changed." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"All opening hours have been fetched from Hauki up until this date. Opening " +"hours are fetched until the last day of the month two years from now." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "Reservable Time Spans updated." +msgstr "" + #: tilavarauspalvelu/admin/payment_merchant/admin.py msgid "The Paytrail Merchant ID should be a six-digit number." msgstr "" @@ -3027,6 +2958,71 @@ msgstr "" msgid "Notification manager" msgstr "" +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Open" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Closed" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Undefined" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Self service" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "With key" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "With reservation" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Open and reservable" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "With key and reservation" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Enter only" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Exit only" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Weather permitting" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Not in use" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "HaukiResourceState" +msgid "Maintenance" +msgstr "" + #: tilavarauspalvelu/models/email_template/model.py msgid "Email type" msgstr "" @@ -3085,6 +3081,10 @@ msgstr "" msgid "Must be the sum of net and vat amounts" msgstr "" +#: tilavarauspalvelu/models/reservable_time_span/model.py +msgid "`start_datetime` must be before `end_datetime`." +msgstr "" + #: tilavarauspalvelu/models/terms_of_use/model.py msgctxt "singular" msgid "terms of use" diff --git a/opening_hours/admin/__init__.py b/opening_hours/admin/__init__.py deleted file mode 100644 index 6fe5b400f..000000000 --- a/opening_hours/admin/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .origin_hauki_resource import OriginHaukiResourceAdmin - -__all__ = [ - "OriginHaukiResourceAdmin", -] diff --git a/opening_hours/apps.py b/opening_hours/apps.py deleted file mode 100644 index 07fdbf7da..000000000 --- a/opening_hours/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class OpeningHoursConfig(AppConfig): - name = "opening_hours" diff --git a/opening_hours/enums.py b/opening_hours/enums.py deleted file mode 100644 index 7caf1d240..000000000 --- a/opening_hours/enums.py +++ /dev/null @@ -1,91 +0,0 @@ -from types import DynamicClassAttribute - -from django.utils.translation import pgettext_lazy -from enumfields import Enum - - -class HaukiResourceState(Enum): - OPEN = "open" - CLOSED = "closed" - UNDEFINED = "undefined" - SELF_SERVICE = "self_service" - WITH_KEY = "with_key" - WITH_RESERVATION = "with_reservation" - OPEN_AND_RESERVABLE = "open_and_reservable" - WITH_KEY_AND_RESERVATION = "with_key_and_reservation" - ENTER_ONLY = "enter_only" - EXIT_ONLY = "exit_only" - WEATHER_PERMITTING = "weather_permitting" - NOT_IN_USE = "not_in_use" - MAINTENANCE = "maintenance" - - class Labels: - OPEN = pgettext_lazy("HaukiResourceState", "Open") - CLOSED = pgettext_lazy("HaukiResourceState", "Closed") - UNDEFINED = pgettext_lazy("HaukiResourceState", "Undefined") - SELF_SERVICE = pgettext_lazy("HaukiResourceState", "Self service") - WITH_KEY = pgettext_lazy("HaukiResourceState", "With key") - WITH_RESERVATION = pgettext_lazy("HaukiResourceState", "With reservation") - OPEN_AND_RESERVABLE = pgettext_lazy("HaukiResourceState", "Open and reservable") - WITH_KEY_AND_RESERVATION = pgettext_lazy("HaukiResourceState", "With key and reservation") - ENTER_ONLY = pgettext_lazy("HaukiResourceState", "Enter only") - EXIT_ONLY = pgettext_lazy("HaukiResourceState", "Exit only") - WEATHER_PERMITTING = pgettext_lazy("HaukiResourceState", "Weather permitting") - NOT_IN_USE = pgettext_lazy("HaukiResourceState", "Not in use") - MAINTENANCE = pgettext_lazy("HaukiResourceState", "Maintenance") - - @classmethod - def accessible_states(cls): - """ - States indicating the space can be accessed in some way, - whether the access is restricted (e.g. via key or reservation) - or not. - """ - return [ - cls.ENTER_ONLY, - cls.OPEN, - cls.OPEN_AND_RESERVABLE, - cls.SELF_SERVICE, - cls.WITH_KEY, - cls.WITH_KEY_AND_RESERVATION, - cls.WITH_RESERVATION, - ] - - @classmethod - def reservable_states(cls): - """States indicating the space can be reserved in some way.""" - return [ - cls.OPEN_AND_RESERVABLE, - cls.WITH_KEY_AND_RESERVATION, - cls.WITH_RESERVATION, - ] - - @classmethod - def closed_states(cls): - """States indicating the space is closed and inaccessible.""" - return [ - None, - cls.CLOSED, - cls.MAINTENANCE, - cls.NOT_IN_USE, - cls.UNDEFINED, - ] - - @DynamicClassAttribute - def is_accessible(self) -> bool: - return self in HaukiResourceState.accessible_states() - - @DynamicClassAttribute - def is_reservable(self) -> bool: - return self in HaukiResourceState.reservable_states() - - @DynamicClassAttribute - def is_closed(self) -> bool: - return self in HaukiResourceState.closed_states() - - @classmethod - def get(cls, state): - try: - return HaukiResourceState(state) - except ValueError: - return HaukiResourceState.UNDEFINED diff --git a/opening_hours/errors.py b/opening_hours/errors.py deleted file mode 100644 index bae8afb96..000000000 --- a/opening_hours/errors.py +++ /dev/null @@ -1,21 +0,0 @@ -from utils.external_service.errors import ExternalServiceError - - -class HaukiAPIError(ExternalServiceError): - """Request succeeded but Hauki API returned an error""" - - -class HaukiConfigurationError(ExternalServiceError): - """Hauki API settings are not configured correctly""" - - -class ReservableTimeSpanClientError(Exception): - pass - - -class ReservableTimeSpanClientValueError(ReservableTimeSpanClientError): - pass - - -class ReservableTimeSpanClientNothingToDoError(ReservableTimeSpanClientError): - pass diff --git a/opening_hours/migrations/0005_remove_reservabletimespan_resource_and_more.py b/opening_hours/migrations/0005_remove_reservabletimespan_resource_and_more.py new file mode 100644 index 000000000..f82807213 --- /dev/null +++ b/opening_hours/migrations/0005_remove_reservabletimespan_resource_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.1 on 2024-09-19 11:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("opening_hours", "0004_alter_originhaukiresource_options"), + ("reservation_units", "0110_alter_reservationunit_origin_hauki_resource"), + ("tilavarauspalvelu", "0010_migrate_opening_hours"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name="reservabletimespan", + name="resource", + ), + migrations.DeleteModel( + name="OriginHaukiResource", + ), + migrations.DeleteModel( + name="ReservableTimeSpan", + ), + ], + database_operations=[], + ), + ] diff --git a/opening_hours/tasks.py b/opening_hours/tasks.py deleted file mode 100644 index 76670bae8..000000000 --- a/opening_hours/tasks.py +++ /dev/null @@ -1,12 +0,0 @@ -import logging - -from config.celery import app -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater - -logger = logging.getLogger(__name__) - - -@app.task(name="update_origin_hauki_resource_reservable_time_spans") -def update_origin_hauki_resource_reservable_time_spans() -> None: - logger.info("Updating OriginHaukiResource reservable time spans...") - HaukiResourceHashUpdater().run() diff --git a/reservation_units/admin/reservation_unit/admin.py b/reservation_units/admin/reservation_unit/admin.py index a332c2e5f..be45fe376 100644 --- a/reservation_units/admin/reservation_unit/admin.py +++ b/reservation_units/admin/reservation_unit/admin.py @@ -10,11 +10,11 @@ from applications.models import ApplicationRoundTimeSlot from common.typing import WSGIRequest -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater from reservation_units.admin.reservation_unit.form import ApplicationRoundTimeSlotForm, ReservationUnitAdminForm from reservation_units.enums import ReservationKind from reservation_units.models import ReservationUnit, ReservationUnitImage, ReservationUnitPricing from reservation_units.utils.export_data import ReservationUnitExporter +from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater __all__ = [ "ReservationUnitAdmin", diff --git a/reservation_units/migrations/0110_alter_reservationunit_origin_hauki_resource.py b/reservation_units/migrations/0110_alter_reservationunit_origin_hauki_resource.py new file mode 100644 index 000000000..d8d97ddcf --- /dev/null +++ b/reservation_units/migrations/0110_alter_reservationunit_origin_hauki_resource.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.1 on 2024-09-19 11:15 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reservation_units", "0109_alter_reservationunit_spaces_and_more"), + ("tilavarauspalvelu", "0010_migrate_opening_hours"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="reservationunit", + name="origin_hauki_resource", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_units", + to="tilavarauspalvelu.originhaukiresource", + ), + ), + ], + database_operations=[], + ), + ] diff --git a/reservation_units/models/reservation_unit.py b/reservation_units/models/reservation_unit.py index 6f96c955e..4f2a3f510 100644 --- a/reservation_units/models/reservation_unit.py +++ b/reservation_units/models/reservation_unit.py @@ -25,10 +25,16 @@ from reservation_units.querysets import ReservationUnitQuerySet if TYPE_CHECKING: - from opening_hours.models import OriginHaukiResource from reservation_units.models import ReservationUnitCancellationRule, ReservationUnitType from reservations.models import ReservationMetadataSet - from tilavarauspalvelu.models import PaymentAccounting, PaymentMerchant, PaymentProduct, TermsOfUse, Unit + from tilavarauspalvelu.models import ( + OriginHaukiResource, + PaymentAccounting, + PaymentMerchant, + PaymentProduct, + TermsOfUse, + Unit, + ) __all__ = [ "ReservationUnit", @@ -120,7 +126,7 @@ class ReservationUnit(SearchDocumentMixin, models.Model): on_delete=models.SET_NULL, ) origin_hauki_resource: OriginHaukiResource | None = models.ForeignKey( - "opening_hours.OriginHaukiResource", + "tilavarauspalvelu.OriginHaukiResource", related_name="reservation_units", on_delete=models.SET_NULL, blank=True, diff --git a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py index 58dd64c91..35e887f00 100644 --- a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py +++ b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py @@ -14,13 +14,13 @@ from applications.enums import ApplicationRoundStatusChoice from applications.models import ApplicationRound from common.date_utils import local_datetime, local_datetime_max, local_datetime_min, local_start_of_day -from opening_hours.models import ReservableTimeSpan -from opening_hours.utils.time_span_element import TimeSpanElement -from opening_hours.utils.time_span_element_utils import merge_overlapping_time_span_elements from reservation_units.utils.first_reservable_time_helper.first_reservable_time_reservation_unit_helper import ( ReservationUnitFirstReservableTimeHelper, ) from reservations.models import AffectingTimeSpan +from tilavarauspalvelu.models import ReservableTimeSpan +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement +from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import merge_overlapping_time_span_elements if TYPE_CHECKING: from decimal import Decimal diff --git a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservable_time_span_helper.py b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservable_time_span_helper.py index 9967656f1..64dcf98e7 100644 --- a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservable_time_span_helper.py +++ b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservable_time_span_helper.py @@ -4,16 +4,16 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING -from opening_hours.utils.time_span_element_utils import override_reservable_with_closed_time_spans from reservation_units.enums import ReservationStartInterval from reservation_units.utils.first_reservable_time_helper.utils import ReservableTimeOutput +from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import override_reservable_with_closed_time_spans if TYPE_CHECKING: - from opening_hours.models import ReservableTimeSpan - from opening_hours.utils.time_span_element import TimeSpanElement from reservation_units.utils.first_reservable_time_helper.first_reservable_time_reservation_unit_helper import ( ReservationUnitFirstReservableTimeHelper, ) + from tilavarauspalvelu.models import ReservableTimeSpan + from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement class ReservableTimeSpanFirstReservableTimeHelper: diff --git a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservation_unit_helper.py b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservation_unit_helper.py index 8e18f53d3..1df86ec44 100644 --- a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservation_unit_helper.py +++ b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_reservation_unit_helper.py @@ -4,13 +4,13 @@ from typing import TYPE_CHECKING from common.date_utils import local_datetime, local_datetime_max, local_datetime_min, local_start_of_day -from opening_hours.utils.time_span_element import TimeSpanElement -from opening_hours.utils.time_span_element_utils import merge_overlapping_time_span_elements from reservation_units.enums import ReservationStartInterval from reservation_units.utils.first_reservable_time_helper.first_reservable_time_reservable_time_span_helper import ( ReservableTimeSpanFirstReservableTimeHelper, ) from reservation_units.utils.first_reservable_time_helper.utils import ReservableTimeOutput +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement +from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import merge_overlapping_time_span_elements if TYPE_CHECKING: from reservation_units.models.reservation_unit import ReservationUnit diff --git a/reservations/models/affecting_time_span.py b/reservations/models/affecting_time_span.py index 3ed83e584..241872288 100644 --- a/reservations/models/affecting_time_span.py +++ b/reservations/models/affecting_time_span.py @@ -12,8 +12,8 @@ from django.utils.translation import gettext_lazy as _ from common.date_utils import DEFAULT_TIMEZONE, local_datetime, timedelta_to_json -from opening_hours.utils.time_span_element import TimeSpanElement from reservations.querysets import AffectingTimeSpanQuerySet +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement from utils.sentry import SentryLogger if TYPE_CHECKING: diff --git a/tests/factories/opening_hours.py b/tests/factories/opening_hours.py index a9ececbd9..42f2ac030 100644 --- a/tests/factories/opening_hours.py +++ b/tests/factories/opening_hours.py @@ -1,6 +1,6 @@ import factory -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan +from tilavarauspalvelu.models import OriginHaukiResource, ReservableTimeSpan from ._base import GenericDjangoModelFactory diff --git a/tests/test_actions/test_reservation_unit_actions_hauki_exporter.py b/tests/test_actions/test_reservation_unit_actions_hauki_exporter.py index 0b1d75ce5..d78b58d3f 100644 --- a/tests/test_actions/test_reservation_unit_actions_hauki_exporter.py +++ b/tests/test_actions/test_reservation_unit_actions_hauki_exporter.py @@ -1,10 +1,10 @@ import pytest -from opening_hours.errors import HaukiAPIError -from opening_hours.utils.hauki_api_client import HaukiAPIClient from reservation_units.models import ReservationUnit from tests.factories import OriginHaukiResourceFactory, ReservationUnitFactory from tests.helpers import patch_method +from tilavarauspalvelu.exceptions import HaukiAPIError +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_hauki/conftest.py b/tests/test_external_services/test_hauki/conftest.py index 2b56fae18..f6c9e140b 100644 --- a/tests/test_external_services/test_hauki/conftest.py +++ b/tests/test_external_services/test_hauki/conftest.py @@ -9,10 +9,11 @@ @pytest.fixture(autouse=True) def _force_HaukiAPIClient_to_be_mocked(): """Force 'HaukiAPIClient.generic' to be mocked in all tests.""" - with mock.patch( - "opening_hours.utils.hauki_api_client.HaukiAPIClient.generic", - side_effect=NotImplementedError("'HaukiAPIClient.generic' must be mocked!"), - ): + from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient + + path = HaukiAPIClient.__module__ + "." + HaukiAPIClient.__qualname__ + ".generic" + exception = NotImplementedError("'HaukiAPIClient.generic' must be mocked!") + with mock.patch(path, side_effect=exception): yield diff --git a/tests/test_external_services/test_hauki/test_hauki_link_generator.py b/tests/test_external_services/test_hauki/test_hauki_link_generator.py index 8761e079a..2f88f314d 100644 --- a/tests/test_external_services/test_hauki/test_hauki_link_generator.py +++ b/tests/test_external_services/test_hauki/test_hauki_link_generator.py @@ -5,7 +5,7 @@ from django.conf import settings from freezegun import freeze_time -from opening_hours.utils.hauki_link_generator import generate_hauki_link +from tilavarauspalvelu.utils.opening_hours.hauki_link_generator import generate_hauki_link VALID_SIGNATURE = "87cbd7cc4f1730cb71094d2648e79045d0dde31e1025695b69f4d76c301e4f20" ORGANIZATION = settings.HAUKI_ORGANISATION_ID diff --git a/tests/test_external_services/test_hauki/test_hauki_resource_hash_updater.py b/tests/test_external_services/test_hauki/test_hauki_resource_hash_updater.py index dae71dceb..16d85e2de 100644 --- a/tests/test_external_services/test_hauki/test_hauki_resource_hash_updater.py +++ b/tests/test_external_services/test_hauki/test_hauki_resource_hash_updater.py @@ -1,24 +1,27 @@ import datetime +from typing import TYPE_CHECKING import freezegun import pytest -from django.utils.timezone import get_default_timezone -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater -from opening_hours.utils.reservable_time_span_client import NEVER_ANY_OPENING_HOURS_HASH, ReservableTimeSpanClient +from common.date_utils import DEFAULT_TIMEZONE from tests.factories.opening_hours import OriginHaukiResourceFactory, ReservableTimeSpanFactory from tests.helpers import patch_method from tests.mocks import MockResponse +from tilavarauspalvelu.constants import NEVER_ANY_OPENING_HOURS_HASH +from tilavarauspalvelu.models import ReservableTimeSpan +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater +from tilavarauspalvelu.utils.opening_hours.reservable_time_span_client import ReservableTimeSpanClient + +if TYPE_CHECKING: + from tilavarauspalvelu.models import OriginHaukiResource # Applied to all tests pytestmark = [ pytest.mark.django_db, ] -DEFAULT_TIMEZONE = get_default_timezone() - ############ # __init__ # ############ diff --git a/tests/test_external_services/test_hauki/test_merge_overlapping_time_span_elements.py b/tests/test_external_services/test_hauki/test_merge_overlapping_time_span_elements.py index a9d3016f1..b743c4aee 100644 --- a/tests/test_external_services/test_hauki/test_merge_overlapping_time_span_elements.py +++ b/tests/test_external_services/test_hauki/test_merge_overlapping_time_span_elements.py @@ -1,8 +1,8 @@ from datetime import timedelta -from opening_hours.utils.time_span_element import TimeSpanElement -from opening_hours.utils.time_span_element_utils import merge_overlapping_time_span_elements from tests.test_external_services.test_hauki.test_reservable_time_spans_client import _get_date +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement +from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import merge_overlapping_time_span_elements # No buffers diff --git a/tests/test_external_services/test_hauki/test_override_reservable_with_closed_time_spans.py b/tests/test_external_services/test_hauki/test_override_reservable_with_closed_time_spans.py index 15135cc87..64975fab3 100644 --- a/tests/test_external_services/test_hauki/test_override_reservable_with_closed_time_spans.py +++ b/tests/test_external_services/test_hauki/test_override_reservable_with_closed_time_spans.py @@ -1,10 +1,10 @@ -from opening_hours.utils.time_span_element import TimeSpanElement -from opening_hours.utils.time_span_element_utils import override_reservable_with_closed_time_spans from tests.test_external_services.test_hauki.test_reservable_time_spans_client import ( _get_date, _get_normalised_time_spans, _get_reservable_and_closed_time_spans, ) +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement +from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import override_reservable_with_closed_time_spans def test__override_reservable_with_closed_time_spans(): diff --git a/tests/test_external_services/test_hauki/test_reservable_time_spans_client.py b/tests/test_external_services/test_hauki/test_reservable_time_spans_client.py index f613a95d9..5c077ca39 100644 --- a/tests/test_external_services/test_hauki/test_reservable_time_spans_client.py +++ b/tests/test_external_services/test_hauki/test_reservable_time_spans_client.py @@ -6,21 +6,21 @@ from freezegun import freeze_time from common.date_utils import DEFAULT_TIMEZONE -from opening_hours.enums import HaukiResourceState -from opening_hours.errors import ReservableTimeSpanClientNothingToDoError -from opening_hours.models import ReservableTimeSpan -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_api_types import ( +from tests.helpers import patch_method +from tests.mocks import MockResponse +from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.exceptions import ReservableTimeSpanClientNothingToDoError +from tilavarauspalvelu.models import ReservableTimeSpan +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import ( HaukiAPIOpeningHoursResponseDate, HaukiAPIOpeningHoursResponseItem, HaukiAPIOpeningHoursResponseResource, HaukiAPIOpeningHoursResponseTime, HaukiTranslatedField, ) -from opening_hours.utils.reservable_time_span_client import ReservableTimeSpanClient -from opening_hours.utils.time_span_element import TimeSpanElement -from tests.helpers import patch_method -from tests.mocks import MockResponse +from tilavarauspalvelu.utils.opening_hours.reservable_time_span_client import ReservableTimeSpanClient +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_hauki/test_summaries.py b/tests/test_external_services/test_hauki/test_summaries.py index 4819e7a2f..8305abccb 100644 --- a/tests/test_external_services/test_hauki/test_summaries.py +++ b/tests/test_external_services/test_hauki/test_summaries.py @@ -3,8 +3,8 @@ import pytest from django.utils.timezone import get_default_timezone -from opening_hours.utils.summaries import get_resources_total_hours_per_resource from tests.factories import OriginHaukiResourceFactory, ReservableTimeSpanFactory +from tilavarauspalvelu.utils.opening_hours.summaries import get_resources_total_hours_per_resource # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_hauki/test_time_span_element.py b/tests/test_external_services/test_hauki/test_time_span_element.py index af4f9352f..23d8afdae 100644 --- a/tests/test_external_services/test_hauki/test_time_span_element.py +++ b/tests/test_external_services/test_hauki/test_time_span_element.py @@ -5,10 +5,10 @@ from django.utils.timezone import get_default_timezone from graphene_django_extensions.testing.utils import parametrize_helper -from opening_hours.enums import HaukiResourceState -from opening_hours.utils.hauki_api_types import HaukiAPIOpeningHoursResponseTime -from opening_hours.utils.time_span_element import TimeSpanElement from tests.test_external_services.test_hauki.test_reservable_time_spans_client import _get_date +from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIOpeningHoursResponseTime +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tests/test_external_services/test_tprek/test_tprek_unit_hauki_resource_importer.py b/tests/test_external_services/test_tprek/test_tprek_unit_hauki_resource_importer.py index debc06a77..584e7e2e5 100644 --- a/tests/test_external_services/test_tprek/test_tprek_unit_hauki_resource_importer.py +++ b/tests/test_external_services/test_tprek/test_tprek_unit_hauki_resource_importer.py @@ -1,10 +1,10 @@ import pytest -from opening_hours.utils.hauki_api_client import HaukiAPIClient from tests.factories import OriginHaukiResourceFactory, UnitFactory from tests.helpers import patch_method from tests.mocks import MockResponse from tilavarauspalvelu.utils.importers.tprek_unit_importer import TprekUnitHaukiResourceIdImporter +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient pytestmark = [ pytest.mark.django_db, diff --git a/tests/test_graphql_api/test_reservation_unit/test_create.py b/tests/test_graphql_api/test_reservation_unit/test_create.py index ed95e5981..e08778092 100644 --- a/tests/test_graphql_api/test_reservation_unit/test_create.py +++ b/tests/test_graphql_api/test_reservation_unit/test_create.py @@ -3,13 +3,13 @@ import pytest from applications.enums import WeekdayChoice -from opening_hours.errors import HaukiAPIError -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_api_types import HaukiAPIResource, HaukiTranslatedField from reservation_units.enums import ReservationKind from reservation_units.models import ReservationUnit from tests.factories import UnitFactory from tests.helpers import patch_method +from tilavarauspalvelu.exceptions import HaukiAPIError +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIResource, HaukiTranslatedField from .helpers import CREATE_MUTATION, get_create_non_draft_input_data diff --git a/tests/test_graphql_api/test_reservation_unit/test_hauki_integration.py b/tests/test_graphql_api/test_reservation_unit/test_hauki_integration.py index 378359669..ff8e1ddfb 100644 --- a/tests/test_graphql_api/test_reservation_unit/test_hauki_integration.py +++ b/tests/test_graphql_api/test_reservation_unit/test_hauki_integration.py @@ -5,10 +5,10 @@ from graphql_relay import to_global_id from actions.reservation_unit import ReservationUnitHaukiExporter -from opening_hours.errors import HaukiAPIError -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater from tests.factories import OriginHaukiResourceFactory, ReservationUnitFactory from tests.helpers import patch_method +from tilavarauspalvelu.exceptions import HaukiAPIError +from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater from .helpers import UPDATE_MUTATION, get_draft_update_input_data, reservation_unit_query diff --git a/tests/test_querysets/test_reservable_time_span.py b/tests/test_querysets/test_reservable_time_span.py index 5848e0260..c651b358f 100644 --- a/tests/test_querysets/test_reservable_time_span.py +++ b/tests/test_querysets/test_reservable_time_span.py @@ -4,8 +4,8 @@ import pytest from graphene_django_extensions.testing.utils import parametrize_helper -from opening_hours.models import ReservableTimeSpan from tests.factories import OriginHaukiResourceFactory, ReservableTimeSpanFactory +from tilavarauspalvelu.models import ReservableTimeSpan # Applied to all tests pytestmark = [ diff --git a/tests/test_querysets/test_reservation_querysets.py b/tests/test_querysets/test_reservation_querysets.py index 8932d2fb9..5645309b3 100644 --- a/tests/test_querysets/test_reservation_querysets.py +++ b/tests/test_querysets/test_reservation_querysets.py @@ -4,11 +4,11 @@ import pytest from common.date_utils import DEFAULT_TIMEZONE, local_date -from opening_hours.utils.time_span_element import TimeSpanElement from reservation_units.models import ReservationUnit, ReservationUnitHierarchy from reservations.enums import ReservationStateChoice, ReservationTypeChoice from reservations.models import AffectingTimeSpan, Reservation from tests.factories import ReservationFactory, ReservationUnitFactory, ResourceFactory, SpaceFactory, UnitFactory +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement # Applied to all tests pytestmark = [ diff --git a/tests/test_utils/test_first_reservable_time_helper.py b/tests/test_utils/test_first_reservable_time_helper.py index 30ceb04c4..6824f665a 100644 --- a/tests/test_utils/test_first_reservable_time_helper.py +++ b/tests/test_utils/test_first_reservable_time_helper.py @@ -7,7 +7,6 @@ from django.utils.timezone import get_default_timezone from graphene_django_extensions.testing.utils import parametrize_helper -from opening_hours.utils.time_span_element import TimeSpanElement from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnit from reservation_units.utils.first_reservable_time_helper.first_reservable_time_helper import FirstReservableTimeHelper @@ -18,6 +17,7 @@ ReservationUnitFirstReservableTimeHelper, ) from tests.factories import OriginHaukiResourceFactory, ReservableTimeSpanFactory, ReservationUnitFactory +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tests/test_utils/test_generate_reservations_from_allocations.py b/tests/test_utils/test_generate_reservations_from_allocations.py index 7a2430ebc..b225d474b 100644 --- a/tests/test_utils/test_generate_reservations_from_allocations.py +++ b/tests/test_utils/test_generate_reservations_from_allocations.py @@ -7,9 +7,6 @@ from applications.enums import ApplicantTypeChoice, Weekday from applications.tasks import generate_reservation_series_from_allocations from common.date_utils import DEFAULT_TIMEZONE, combine, local_date, local_datetime, local_iso_format -from opening_hours.enums import HaukiResourceState -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_api_types import HaukiAPIDatePeriod from reservation_units.models import ReservationUnitHierarchy from reservations.enums import ( CustomerTypeChoice, @@ -20,6 +17,9 @@ from reservations.models import AffectingTimeSpan, RecurringReservation, RejectedOccurrence, Reservation from tests.factories import AllocatedTimeSlotFactory, ReservationFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIDatePeriod from utils.sentry import SentryLogger pytestmark = [ diff --git a/tilavarauspalvelu/admin/__init__.py b/tilavarauspalvelu/admin/__init__.py index b2f3f30a0..6cc1b760c 100644 --- a/tilavarauspalvelu/admin/__init__.py +++ b/tilavarauspalvelu/admin/__init__.py @@ -1,5 +1,6 @@ from .email_template.admin import EmailTemplateAdmin from .general_role.admin import GeneralRoleAdmin +from .origin_hauki_resource.admin import OriginHaukiResourceAdmin from .payment_accounting.admin import PaymentAccountingAdmin from .payment_merchant.admin import PaymentMerchantAdmin from .payment_order.admin import PaymentOrderAdmin @@ -16,6 +17,7 @@ __all__ = [ "EmailTemplateAdmin", "GeneralRoleAdmin", + "OriginHaukiResourceAdmin", "PaymentAccountingAdmin", "PaymentMerchantAdmin", "PaymentOrderAdmin", diff --git a/tilavarauspalvelu/admin/hauki_resource/admin.py b/tilavarauspalvelu/admin/hauki_resource/admin.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/opening_hours/management/__init__.py b/tilavarauspalvelu/admin/origin_hauki_resource/__init__.py similarity index 100% rename from opening_hours/management/__init__.py rename to tilavarauspalvelu/admin/origin_hauki_resource/__init__.py diff --git a/opening_hours/admin/origin_hauki_resource.py b/tilavarauspalvelu/admin/origin_hauki_resource/admin.py similarity index 75% rename from opening_hours/admin/origin_hauki_resource.py rename to tilavarauspalvelu/admin/origin_hauki_resource/admin.py index 38ba1bb50..c9324746b 100644 --- a/opening_hours/admin/origin_hauki_resource.py +++ b/tilavarauspalvelu/admin/origin_hauki_resource/admin.py @@ -3,51 +3,20 @@ from django import forms from django.contrib import admin from django.db.models import Count, QuerySet -from django.urls import reverse -from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from common.typing import WSGIRequest -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater -from opening_hours.utils.reservable_time_span_client import NEVER_ANY_OPENING_HOURS_HASH -from reservation_units.models import ReservationUnit +from tilavarauspalvelu.admin.reservable_time_span.admin import ReservableTimeSpanInline +from tilavarauspalvelu.admin.reservation_unit.admin import ReservationUnitInline +from tilavarauspalvelu.constants import NEVER_ANY_OPENING_HOURS_HASH +from tilavarauspalvelu.models import OriginHaukiResource +from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater __all__ = [ "OriginHaukiResourceAdmin", ] -class ReservationUnitInline(admin.TabularInline): - model = ReservationUnit - fields = ["id", "reservation_unit_link"] - readonly_fields = fields - can_delete = False - extra = 0 - - def has_add_permission(self, request, obj=None) -> bool: - return False - - def reservation_unit_link(self, obj): - url = reverse("admin:reservation_units_reservationunit_change", args=(obj.pk,)) - - return format_html(f"{obj.name_fi}") - - -class ReservableTimeSpanInline(admin.TabularInline): - model = ReservableTimeSpan - fields = ["time_span_str"] - readonly_fields = fields - can_delete = False - extra = 0 - - def has_add_permission(self, request, obj=None) -> bool: - return False - - def time_span_str(self, obj: ReservableTimeSpan) -> str: - return obj.get_datetime_str() - - class OriginHaukiResourceAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/opening_hours/management/commands/__init__.py b/tilavarauspalvelu/admin/reservable_time_span/__init__.py similarity index 100% rename from opening_hours/management/commands/__init__.py rename to tilavarauspalvelu/admin/reservable_time_span/__init__.py diff --git a/tilavarauspalvelu/admin/reservable_time_span/admin.py b/tilavarauspalvelu/admin/reservable_time_span/admin.py new file mode 100644 index 000000000..0e74d9000 --- /dev/null +++ b/tilavarauspalvelu/admin/reservable_time_span/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from tilavarauspalvelu.models import ReservableTimeSpan + + +class ReservableTimeSpanInline(admin.TabularInline): + model = ReservableTimeSpan + fields = ["time_span_str"] + readonly_fields = fields + can_delete = False + extra = 0 + + def has_add_permission(self, request, obj=None) -> bool: + return False + + def time_span_str(self, obj: ReservableTimeSpan) -> str: + return obj.get_datetime_str() diff --git a/tilavarauspalvelu/admin/reservable_timespan/admin.py b/tilavarauspalvelu/admin/reservable_timespan/admin.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/admin/reservation_unit/admin.py b/tilavarauspalvelu/admin/reservation_unit/admin.py index e69de29bb..9909ebdaf 100644 --- a/tilavarauspalvelu/admin/reservation_unit/admin.py +++ b/tilavarauspalvelu/admin/reservation_unit/admin.py @@ -0,0 +1,21 @@ +from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html + +from reservation_units.models import ReservationUnit + + +class ReservationUnitInline(admin.TabularInline): + model = ReservationUnit + fields = ["id", "reservation_unit_link"] + readonly_fields = fields + can_delete = False + extra = 0 + + def has_add_permission(self, request, obj=None) -> bool: + return False + + def reservation_unit_link(self, obj): + url = reverse("admin:reservation_units_reservationunit_change", args=(obj.pk,)) + + return format_html(f"{obj.name_fi}") diff --git a/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py b/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py index b81e02258..95e77cb6c 100644 --- a/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py +++ b/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py @@ -13,7 +13,6 @@ from applications.enums import WeekdayChoice from common.date_utils import local_date from common.fields.serializer import CurrentUserDefaultNullable, input_only_field -from opening_hours.utils.reservable_time_span_client import ReservableTimeSpanClient from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnit from reservations.enums import ReservationStateChoice, ReservationTypeStaffChoice @@ -25,6 +24,8 @@ "RecurringReservationCreateSerializer", ] +from tilavarauspalvelu.utils.opening_hours.reservable_time_span_client import ReservableTimeSpanClient + class RecurringReservationCreateSerializer(NestingModelSerializer): instance: None diff --git a/tilavarauspalvelu/api/graphql/types/reservation_unit/serializers.py b/tilavarauspalvelu/api/graphql/types/reservation_unit/serializers.py index 9e710196d..f305321a0 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_unit/serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_unit/serializers.py @@ -8,7 +8,6 @@ from rest_framework.exceptions import ValidationError from applications.enums import WeekdayChoice -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater from reservation_units.enums import PricingStatus, ReservationStartInterval from reservation_units.models import ReservationUnit, ReservationUnitPricing from reservation_units.utils.reservation_unit_pricing_helper import ReservationUnitPricingHelper @@ -18,6 +17,7 @@ ) from tilavarauspalvelu.api.graphql.types.reservation_unit_image.serializers import ReservationUnitImageFieldSerializer from tilavarauspalvelu.api.graphql.types.reservation_unit_pricing.serializers import ReservationUnitPricingSerializer +from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater from utils.external_service.errors import ExternalServiceError if TYPE_CHECKING: diff --git a/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py b/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py index 2791a205d..2607bc674 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py @@ -13,15 +13,14 @@ from common.db import SubqueryCount from common.typing import GQLInfo -from opening_hours.models import OriginHaukiResource -from opening_hours.utils.hauki_link_generator import generate_hauki_link from reservation_units.enums import ReservationUnitPublishingState, ReservationUnitReservationState from reservation_units.models import ReservationUnit from reservations.enums import ReservationTypeChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.types.location.types import LocationNode from tilavarauspalvelu.api.graphql.types.reservation.types import ReservationNode -from tilavarauspalvelu.models import Location, PaymentMerchant, Space, Unit +from tilavarauspalvelu.models import Location, OriginHaukiResource, PaymentMerchant, Space, Unit +from tilavarauspalvelu.utils.opening_hours.hauki_link_generator import generate_hauki_link from .filtersets import ReservationUnitFilterSet from .permissions import ReservationUnitPermission diff --git a/tilavarauspalvelu/constants.py b/tilavarauspalvelu/constants.py index 3a8951fcb..1b21f6991 100644 --- a/tilavarauspalvelu/constants.py +++ b/tilavarauspalvelu/constants.py @@ -10,3 +10,8 @@ # The coordinates in this coordinate system are numbers in the range of # -90.0000 to 90.0000 for latitude and -180.0000 to 180.0000 for longitude. COORDINATE_SYSTEM_ID: int = 4326 + + +# Hash value for when there are never any opening hours +# See https://github.com/City-of-Helsinki/hauki `hours.models.Resource._get_date_periods_as_hash` +NEVER_ANY_OPENING_HOURS_HASH = "d41d8cd98f00b204e9800998ecf8427e" # md5(b"").hexdigest() diff --git a/tilavarauspalvelu/enums.py b/tilavarauspalvelu/enums.py index acfc486c3..153e4738a 100644 --- a/tilavarauspalvelu/enums.py +++ b/tilavarauspalvelu/enums.py @@ -2,11 +2,13 @@ import enum from inspect import cleandoc +from types import DynamicClassAttribute from django.db import models from django.utils.functional import classproperty from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy +from enumfields import Enum from tilavarauspalvelu.typing import permission @@ -227,3 +229,90 @@ class EmailType(models.TextChoices): RESERVATION_WITH_PIN_CONFIRMED = "reservation_with_pin_confirmed" STAFF_NOTIFICATION_RESERVATION_MADE = "staff_notification_reservation_made" STAFF_NOTIFICATION_RESERVATION_REQUIRES_HANDLING = "staff_notification_reservation_requires_handling" + + +class HaukiResourceState(Enum): + OPEN = "open" + CLOSED = "closed" + UNDEFINED = "undefined" + SELF_SERVICE = "self_service" + WITH_KEY = "with_key" + WITH_RESERVATION = "with_reservation" + OPEN_AND_RESERVABLE = "open_and_reservable" + WITH_KEY_AND_RESERVATION = "with_key_and_reservation" + ENTER_ONLY = "enter_only" + EXIT_ONLY = "exit_only" + WEATHER_PERMITTING = "weather_permitting" + NOT_IN_USE = "not_in_use" + MAINTENANCE = "maintenance" + + class Labels: + OPEN = pgettext_lazy("HaukiResourceState", "Open") + CLOSED = pgettext_lazy("HaukiResourceState", "Closed") + UNDEFINED = pgettext_lazy("HaukiResourceState", "Undefined") + SELF_SERVICE = pgettext_lazy("HaukiResourceState", "Self service") + WITH_KEY = pgettext_lazy("HaukiResourceState", "With key") + WITH_RESERVATION = pgettext_lazy("HaukiResourceState", "With reservation") + OPEN_AND_RESERVABLE = pgettext_lazy("HaukiResourceState", "Open and reservable") + WITH_KEY_AND_RESERVATION = pgettext_lazy("HaukiResourceState", "With key and reservation") + ENTER_ONLY = pgettext_lazy("HaukiResourceState", "Enter only") + EXIT_ONLY = pgettext_lazy("HaukiResourceState", "Exit only") + WEATHER_PERMITTING = pgettext_lazy("HaukiResourceState", "Weather permitting") + NOT_IN_USE = pgettext_lazy("HaukiResourceState", "Not in use") + MAINTENANCE = pgettext_lazy("HaukiResourceState", "Maintenance") + + @classmethod + def accessible_states(cls): + """ + States indicating the space can be accessed in some way, + whether the access is restricted (e.g. via key or reservation) + or not. + """ + return [ + cls.ENTER_ONLY, + cls.OPEN, + cls.OPEN_AND_RESERVABLE, + cls.SELF_SERVICE, + cls.WITH_KEY, + cls.WITH_KEY_AND_RESERVATION, + cls.WITH_RESERVATION, + ] + + @classmethod + def reservable_states(cls): + """States indicating the space can be reserved in some way.""" + return [ + cls.OPEN_AND_RESERVABLE, + cls.WITH_KEY_AND_RESERVATION, + cls.WITH_RESERVATION, + ] + + @classmethod + def closed_states(cls): + """States indicating the space is closed and inaccessible.""" + return [ + None, + cls.CLOSED, + cls.MAINTENANCE, + cls.NOT_IN_USE, + cls.UNDEFINED, + ] + + @DynamicClassAttribute + def is_accessible(self) -> bool: + return self in HaukiResourceState.accessible_states() + + @DynamicClassAttribute + def is_reservable(self) -> bool: + return self in HaukiResourceState.reservable_states() + + @DynamicClassAttribute + def is_closed(self) -> bool: + return self in HaukiResourceState.closed_states() + + @classmethod + def get(cls, state): + try: + return HaukiResourceState(state) + except ValueError: + return HaukiResourceState.UNDEFINED diff --git a/tilavarauspalvelu/exceptions.py b/tilavarauspalvelu/exceptions.py index 49c5870e8..cb40e64d2 100644 --- a/tilavarauspalvelu/exceptions.py +++ b/tilavarauspalvelu/exceptions.py @@ -1,3 +1,6 @@ +from utils.external_service.errors import ExternalServiceError + + class SendEmailNotificationError(Exception): pass @@ -10,3 +13,23 @@ class EmailTemplateValidationError(Exception): def __init__(self, *args, **kwargs) -> None: if len(args) > 0: self.message = args[0] + + +class HaukiAPIError(ExternalServiceError): + """Request succeeded but Hauki API returned an error""" + + +class HaukiConfigurationError(ExternalServiceError): + """Hauki API settings are not configured correctly""" + + +class ReservableTimeSpanClientError(Exception): + pass + + +class ReservableTimeSpanClientValueError(ReservableTimeSpanClientError): + pass + + +class ReservableTimeSpanClientNothingToDoError(ReservableTimeSpanClientError): + pass diff --git a/opening_hours/management/commands/update_hauki_hashes.py b/tilavarauspalvelu/management/commands/update_hauki_hashes.py similarity index 76% rename from opening_hours/management/commands/update_hauki_hashes.py rename to tilavarauspalvelu/management/commands/update_hauki_hashes.py index e1a97541e..7a1f909fd 100644 --- a/opening_hours/management/commands/update_hauki_hashes.py +++ b/tilavarauspalvelu/management/commands/update_hauki_hashes.py @@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand -from opening_hours.utils.hauki_resource_hash_updater import HaukiResourceHashUpdater +from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater logger = logging.getLogger(__name__) diff --git a/tilavarauspalvelu/migrations/0010_migrate_opening_hours.py b/tilavarauspalvelu/migrations/0010_migrate_opening_hours.py new file mode 100644 index 000000000..d2863c466 --- /dev/null +++ b/tilavarauspalvelu/migrations/0010_migrate_opening_hours.py @@ -0,0 +1,94 @@ +# Generated by Django 5.1.1 on 2024-09-19 11:15 + +import django.db.models.deletion +from django.db import migrations, models + +from utils.migration import TestOnlyRunBefore + + +class Migration(migrations.Migration): + dependencies = [ + ("tilavarauspalvelu", "0009_migrate_email_template"), + ("applications", "0094_alter_applicationround_terms_of_use"), + ("common", "0012_sqllog_stack_info"), + ("email_notification", "0010_delete_emailtemplate"), + ("merchants", "0019_delete_paymentaccounting_and_more"), + ("opening_hours", "0004_alter_originhaukiresource_options"), + ("permissions", "0035_remove_unitrole_assigner_remove_unitrole_unit_groups_and_more"), + ("reservation_units", "0109_alter_reservationunit_spaces_and_more"), + ("reservations", "0082_remove_net_prices"), + ("resources", "0011_delete_resource"), + ("services", "0006_delete_service"), + ("spaces", "0042_delete_space_delete_unit_delete_unitgroup"), + ("terms_of_use", "0006_delete_termsofuse"), + ("users", "0017_remove_personalinfoviewlog_user_and_more"), + ] + + run_before = TestOnlyRunBefore( + run_before=[ + ("social_django", "0013_migrate_extra_data"), + ], + ) + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name="OriginHaukiResource", + fields=[ + ("id", models.IntegerField(primary_key=True, serialize=False, unique=True)), + ("opening_hours_hash", models.CharField(blank=True, max_length=64)), + ("latest_fetched_date", models.DateField(blank=True, null=True)), + ], + options={ + "db_table": "origin_hauki_resource", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.AlterField( + model_name="unit", + name="origin_hauki_resource", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="units", + to="tilavarauspalvelu.originhaukiresource", + ), + ), + migrations.CreateModel( + name="ReservableTimeSpan", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("start_datetime", models.DateTimeField()), + ("end_datetime", models.DateTimeField()), + ( + "resource", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reservable_time_spans", + to="tilavarauspalvelu.originhaukiresource", + ), + ), + ], + options={ + "db_table": "reservable_time_span", + "ordering": ["resource", "start_datetime", "end_datetime"], + "base_manager_name": "objects", + "constraints": [ + models.CheckConstraint( + condition=models.Q(("start_datetime__lt", models.F("end_datetime"))), + name="reservable_time_span_start_before_end", + violation_error_message="`start_datetime` must be before `end_datetime`.", + ) + ], + }, + ), + ], + database_operations=[], + ), + ] diff --git a/tilavarauspalvelu/models/__init__.py b/tilavarauspalvelu/models/__init__.py index 6ad43cdc4..0c68bfa8e 100644 --- a/tilavarauspalvelu/models/__init__.py +++ b/tilavarauspalvelu/models/__init__.py @@ -2,12 +2,14 @@ from .email_template.model import EmailTemplate from .general_role.model import GeneralRole from .location.model import Location +from .origin_hauki_resource.model import OriginHaukiResource from .payment_accounting.model import PaymentAccounting from .payment_merchant.model import PaymentMerchant from .payment_order.model import PaymentOrder from .payment_product.model import PaymentProduct from .personal_info_view_log.model import PersonalInfoViewLog from .real_estate.model import RealEstate +from .reservable_time_span.model import ReservableTimeSpan from .resource.model import Resource from .service.model import Service from .service_sector.model import ServiceSector @@ -23,6 +25,7 @@ "EmailTemplate", "GeneralRole", "Location", + "OriginHaukiResource", "PaymentAccounting", "PaymentMerchant", "PaymentOrder", @@ -30,6 +33,7 @@ "PersonalInfoViewLog", "ProfileUser", "RealEstate", + "ReservableTimeSpan", "Resource", "Service", "ServiceSector", diff --git a/tilavarauspalvelu/models/hauki_resource/__init__.py b/tilavarauspalvelu/models/hauki_resource/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/hauki_resource/actions.py b/tilavarauspalvelu/models/hauki_resource/actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/hauki_resource/model.py b/tilavarauspalvelu/models/hauki_resource/model.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/hauki_resource/queryset.py b/tilavarauspalvelu/models/hauki_resource/queryset.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/opening_hours/utils/__init__.py b/tilavarauspalvelu/models/origin_hauki_resource/__init__.py similarity index 100% rename from opening_hours/utils/__init__.py rename to tilavarauspalvelu/models/origin_hauki_resource/__init__.py diff --git a/tilavarauspalvelu/models/origin_hauki_resource/actions.py b/tilavarauspalvelu/models/origin_hauki_resource/actions.py new file mode 100644 index 000000000..cbe923d1c --- /dev/null +++ b/tilavarauspalvelu/models/origin_hauki_resource/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import OriginHaukiResource + + +class OriginHaukiResourceActions: + def __init__(self, origin_hauki_resource: "OriginHaukiResource") -> None: + self.origin_hauki_resource = origin_hauki_resource diff --git a/tilavarauspalvelu/models/origin_hauki_resource/model.py b/tilavarauspalvelu/models/origin_hauki_resource/model.py new file mode 100644 index 000000000..709fc959f --- /dev/null +++ b/tilavarauspalvelu/models/origin_hauki_resource/model.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import OriginHaukiResourceManager + +if TYPE_CHECKING: + from datetime import datetime + + from .actions import OriginHaukiResourceActions + + +__all__ = [ + "OriginHaukiResource", +] + + +class OriginHaukiResource(models.Model): + # Resource id in Hauki API + id = models.IntegerField(unique=True, primary_key=True) + # Hauki API hash for opening hours, which is used to determine if the opening hours have changed + opening_hours_hash = models.CharField(max_length=64, blank=True) + # Latest date fetched from Hauki opening hours API + latest_fetched_date = models.DateField(blank=True, null=True) + + objects = OriginHaukiResourceManager() + + class Meta: + db_table = "origin_hauki_resource" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return str(self.id) + + @cached_property + def actions(self) -> OriginHaukiResourceActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import OriginHaukiResourceActions + + return OriginHaukiResourceActions(self) + + def is_reservable(self, start_datetime: datetime, end_datetime: datetime) -> bool: + return self.reservable_time_spans.fully_fill_period(start=start_datetime, end=end_datetime).exists() diff --git a/tilavarauspalvelu/models/origin_hauki_resource/queryset.py b/tilavarauspalvelu/models/origin_hauki_resource/queryset.py new file mode 100644 index 000000000..2e324d49f --- /dev/null +++ b/tilavarauspalvelu/models/origin_hauki_resource/queryset.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "OriginHaukiResourceManager", + "OriginHaukiResourceQuerySet", +] + + +class OriginHaukiResourceQuerySet(models.QuerySet): ... + + +class OriginHaukiResourceManager(models.Manager.from_queryset(OriginHaukiResourceQuerySet)): ... diff --git a/tilavarauspalvelu/admin/hauki_resource/__init__.py b/tilavarauspalvelu/models/reservable_time_span/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/hauki_resource/__init__.py rename to tilavarauspalvelu/models/reservable_time_span/__init__.py diff --git a/tilavarauspalvelu/models/reservable_time_span/actions.py b/tilavarauspalvelu/models/reservable_time_span/actions.py new file mode 100644 index 000000000..9f21a7d54 --- /dev/null +++ b/tilavarauspalvelu/models/reservable_time_span/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservableTimeSpan + + +class ReservableTimeSpanActions: + def __init__(self, reservable_time_span: "ReservableTimeSpan") -> None: + self.reservable_time_span = reservable_time_span diff --git a/opening_hours/models.py b/tilavarauspalvelu/models/reservable_time_span/model.py similarity index 60% rename from opening_hours/models.py rename to tilavarauspalvelu/models/reservable_time_span/model.py index 7331cf71a..565e2ab07 100644 --- a/opening_hours/models.py +++ b/tilavarauspalvelu/models/reservable_time_span/model.py @@ -1,56 +1,38 @@ -from datetime import datetime +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING from django.db import models from django.db.models import F, Q -from django.utils.timezone import get_default_timezone from django.utils.translation import gettext_lazy as _ -from opening_hours.querysets import ReservableTimeSpanQuerySet -from opening_hours.utils.time_span_element import TimeSpanElement +from common.date_utils import DEFAULT_TIMEZONE +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement + +from .queryset import ReservableTimeSpanManager -DEFAULT_TIMEZONE = get_default_timezone() +if TYPE_CHECKING: + from .actions import ReservableTimeSpanActions __all__ = [ - "OriginHaukiResource", "ReservableTimeSpan", ] -class OriginHaukiResource(models.Model): - # Resource id in Hauki API - id = models.IntegerField(unique=True, primary_key=True) - # Hauki API hash for opening hours, which is used to determine if the opening hours have changed - opening_hours_hash = models.CharField(max_length=64, blank=True) - # Latest date fetched from Hauki opening hours API - latest_fetched_date = models.DateField(blank=True, null=True) - - class Meta: - db_table = "origin_hauki_resource" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return str(self.id) - - def is_reservable(self, start_datetime: datetime, end_datetime: datetime) -> bool: - return self.reservable_time_spans.fully_fill_period(start=start_datetime, end=end_datetime).exists() - - class ReservableTimeSpan(models.Model): """A time period on which a ReservationUnit is reservable.""" resource = models.ForeignKey( - OriginHaukiResource, + "tilavarauspalvelu.OriginHaukiResource", related_name="reservable_time_spans", on_delete=models.CASCADE, ) start_datetime = models.DateTimeField(null=False, blank=False) end_datetime = models.DateTimeField(null=False, blank=False) - objects = ReservableTimeSpanQuerySet.as_manager() + objects = ReservableTimeSpanManager() class Meta: db_table = "reservable_time_span" @@ -71,6 +53,14 @@ class Meta: def __str__(self) -> str: return f"{self.resource} {self.get_datetime_str()}" + @cached_property + def actions(self) -> ReservableTimeSpanActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservableTimeSpanActions + + return ReservableTimeSpanActions(self) + def get_datetime_str(self) -> str: strformat = "%Y-%m-%d %H:%M" diff --git a/opening_hours/querysets.py b/tilavarauspalvelu/models/reservable_time_span/queryset.py similarity index 56% rename from opening_hours/querysets.py rename to tilavarauspalvelu/models/reservable_time_span/queryset.py index 33ec93443..f3b81e3fa 100644 --- a/opening_hours/querysets.py +++ b/tilavarauspalvelu/models/reservable_time_span/queryset.py @@ -1,20 +1,22 @@ -from datetime import date, datetime, time, timedelta +import datetime +from typing import Self -from django.db.models import Case, Q, QuerySet, Value, When -from django.utils.timezone import get_default_timezone +from django.db import models -DEFAULT_TIMEZONE = get_default_timezone() +from common.date_utils import normalize_as_datetime +__all__ = [ + "ReservableTimeSpanManager", + "ReservableTimeSpanQuerySet", +] -def _normalize_datetime(value: date | datetime, timedelta_days: int = 0) -> datetime: - if isinstance(value, datetime): - return value - # Convert dates to datetimes to include timezone information - return datetime.combine(value, time.min, tzinfo=DEFAULT_TIMEZONE) + timedelta(days=timedelta_days) - -class ReservableTimeSpanQuerySet(QuerySet): - def overlapping_with_period(self, start: datetime | date, end: datetime | date): +class ReservableTimeSpanQuerySet(models.QuerySet): + def overlapping_with_period( + self, + start: datetime.datetime | datetime.date, + end: datetime.datetime | datetime.date, + ) -> Self: """ Filter to reservable time spans that overlap with the given period. @@ -33,11 +35,15 @@ def overlapping_with_period(self, start: datetime | date, end: datetime | date): │ │----- # No │ │ --- # No """ - start: datetime = _normalize_datetime(start) - end: datetime = _normalize_datetime(end, timedelta_days=1) + start: datetime = normalize_as_datetime(start) + end: datetime = normalize_as_datetime(end, timedelta_days=1) return self.filter(start_datetime__lt=end, end_datetime__gt=start) - def fully_fill_period(self, start: datetime | date, end: datetime | date): + def fully_fill_period( + self, + start: datetime.datetime | datetime.date, + end: datetime.datetime | datetime.date, + ) -> Self: """ Filter to reservable time spans that can fully fill in the given period. @@ -56,35 +62,42 @@ def fully_fill_period(self, start: datetime | date, end: datetime | date): │ │----- # No │ │ --- # No """ - start: datetime = _normalize_datetime(start) - end: datetime = _normalize_datetime(end, timedelta_days=1) + start: datetime = normalize_as_datetime(start) + end: datetime = normalize_as_datetime(end, timedelta_days=1) return self.filter(start_datetime__lte=start, end_datetime__gte=end) - def truncated_start_and_end_datetimes_for_period(self, start: datetime | date, end: datetime | date): + def truncated_start_and_end_datetimes_for_period( + self, + start: datetime.datetime | datetime.date, + end: datetime.datetime | datetime.date, + ) -> Self: """ Annotate truncated start and end datetimes for reservable time spans that overlap with the given period. If the time span starts before the period, the start time is set to the period start. If the time span ends after the period, the end time is set to the period end (start of next day). """ - start = _normalize_datetime(start) - end = _normalize_datetime(end, timedelta_days=1) + start = normalize_as_datetime(start) + end = normalize_as_datetime(end, timedelta_days=1) return self.overlapping_with_period( start=start, end=end, ).annotate( - truncated_start_datetime=Case( - When( - condition=Q(start_datetime__lt=start), - then=Value(start), + truncated_start_datetime=models.Case( + models.When( + condition=models.Q(start_datetime__lt=start), + then=models.Value(start), ), default="start_datetime", ), - truncated_end_datetime=Case( - When( - condition=Q(end_datetime__gt=end), - then=Value(end), + truncated_end_datetime=models.Case( + models.When( + condition=models.Q(end_datetime__gt=end), + then=models.Value(end), ), default="end_datetime", ), ) + + +class ReservableTimeSpanManager(models.Manager.from_queryset(ReservableTimeSpanQuerySet)): ... diff --git a/tilavarauspalvelu/models/reservable_timespan/__init__.py b/tilavarauspalvelu/models/reservable_timespan/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/reservable_timespan/actions.py b/tilavarauspalvelu/models/reservable_timespan/actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/reservable_timespan/model.py b/tilavarauspalvelu/models/reservable_timespan/model.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/reservable_timespan/queryset.py b/tilavarauspalvelu/models/reservable_timespan/queryset.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/unit/model.py b/tilavarauspalvelu/models/unit/model.py index fbd2a7333..b33d71d58 100644 --- a/tilavarauspalvelu/models/unit/model.py +++ b/tilavarauspalvelu/models/unit/model.py @@ -39,7 +39,7 @@ class Unit(models.Model): rank: int | None = models.PositiveIntegerField(blank=True, null=True) # Used for ordering origin_hauki_resource = models.ForeignKey( - "opening_hours.OriginHaukiResource", + "tilavarauspalvelu.OriginHaukiResource", related_name="units", on_delete=models.SET_NULL, blank=True, diff --git a/tilavarauspalvelu/tasks.py b/tilavarauspalvelu/tasks.py index e173c7e12..769108d7c 100644 --- a/tilavarauspalvelu/tasks.py +++ b/tilavarauspalvelu/tasks.py @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.contrib.auth import get_user_model from django.db.transaction import atomic @@ -14,6 +16,8 @@ from tilavarauspalvelu.utils.email.email_sender import EmailNotificationSender from utils.sentry import SentryLogger +logger = logging.getLogger(__name__) + @app.task(name="rebuild_space_tree_hierarchy") def rebuild_space_tree_hierarchy() -> None: @@ -203,3 +207,11 @@ def send_application_handled_email_task() -> None: email_sender.send_batch_application_emails(applications=applications) applications.update(results_ready_notification_sent_date=local_datetime()) + + +@app.task(name="update_origin_hauki_resource_reservable_time_spans") +def update_origin_hauki_resource_reservable_time_spans() -> None: + from tilavarauspalvelu.utils.opening_hours.hauki_resource_hash_updater import HaukiResourceHashUpdater + + logger.info("Updating OriginHaukiResource reservable time spans...") + HaukiResourceHashUpdater().run() diff --git a/opening_hours/decorators.py b/tilavarauspalvelu/utils/decorators.py similarity index 100% rename from opening_hours/decorators.py rename to tilavarauspalvelu/utils/decorators.py diff --git a/opening_hours/hours.py b/tilavarauspalvelu/utils/hours.py similarity index 94% rename from opening_hours/hours.py rename to tilavarauspalvelu/utils/hours.py index c76bdddec..6ac754655 100644 --- a/opening_hours/hours.py +++ b/tilavarauspalvelu/utils/hours.py @@ -1,6 +1,6 @@ import datetime -from opening_hours.models import ReservableTimeSpan +from tilavarauspalvelu.models import ReservableTimeSpan def can_reserve_based_on_opening_hours( diff --git a/tilavarauspalvelu/utils/importers/tprek_unit_importer.py b/tilavarauspalvelu/utils/importers/tprek_unit_importer.py index 0fbfd96b5..49f5b3197 100644 --- a/tilavarauspalvelu/utils/importers/tprek_unit_importer.py +++ b/tilavarauspalvelu/utils/importers/tprek_unit_importer.py @@ -5,14 +5,13 @@ from django.db.models import QuerySet from django.db.transaction import atomic -from opening_hours.models import OriginHaukiResource -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from tilavarauspalvelu.models import Location, Unit +from tilavarauspalvelu.models import Location, OriginHaukiResource, Unit from tilavarauspalvelu.utils.importers.tprek_api_client import TprekAPIClient, TprekLocationData, TprekUnitData +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient from utils.sentry import SentryLogger if TYPE_CHECKING: - from opening_hours.utils.hauki_api_types import HaukiAPIResource + from tilavarauspalvelu.utils.opening_hours import HaukiAPIResource logger = logging.getLogger(__name__) diff --git a/tilavarauspalvelu/admin/reservable_timespan/__init__.py b/tilavarauspalvelu/utils/opening_hours/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/reservable_timespan/__init__.py rename to tilavarauspalvelu/utils/opening_hours/__init__.py diff --git a/opening_hours/utils/hauki_api_client.py b/tilavarauspalvelu/utils/opening_hours/hauki_api_client.py similarity index 97% rename from opening_hours/utils/hauki_api_client.py rename to tilavarauspalvelu/utils/opening_hours/hauki_api_client.py index dba9903fe..12aa8930d 100644 --- a/opening_hours/utils/hauki_api_client.py +++ b/tilavarauspalvelu/utils/opening_hours/hauki_api_client.py @@ -3,8 +3,8 @@ from django.conf import settings -from opening_hours.errors import HaukiAPIError, HaukiConfigurationError -from opening_hours.utils.hauki_api_types import ( +from tilavarauspalvelu.exceptions import HaukiAPIError, HaukiConfigurationError +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import ( HaukiAPIDatePeriod, HaukiAPIOpeningHoursResponse, HaukiAPIOpeningHoursResponseItem, diff --git a/opening_hours/utils/hauki_api_types.py b/tilavarauspalvelu/utils/opening_hours/hauki_api_types.py similarity index 98% rename from opening_hours/utils/hauki_api_types.py rename to tilavarauspalvelu/utils/opening_hours/hauki_api_types.py index c8f18892c..3665ab5c5 100644 --- a/opening_hours/utils/hauki_api_types.py +++ b/tilavarauspalvelu/utils/opening_hours/hauki_api_types.py @@ -1,6 +1,6 @@ from typing import Any, Literal, TypedDict -from opening_hours.enums import HaukiResourceState +from tilavarauspalvelu.enums import HaukiResourceState ########## # Common # diff --git a/opening_hours/utils/hauki_link_generator.py b/tilavarauspalvelu/utils/opening_hours/hauki_link_generator.py similarity index 100% rename from opening_hours/utils/hauki_link_generator.py rename to tilavarauspalvelu/utils/opening_hours/hauki_link_generator.py diff --git a/opening_hours/utils/hauki_resource_hash_updater.py b/tilavarauspalvelu/utils/opening_hours/hauki_resource_hash_updater.py similarity index 90% rename from opening_hours/utils/hauki_resource_hash_updater.py rename to tilavarauspalvelu/utils/opening_hours/hauki_resource_hash_updater.py index cb5a58027..b4fdbc7da 100644 --- a/opening_hours/utils/hauki_resource_hash_updater.py +++ b/tilavarauspalvelu/utils/opening_hours/hauki_resource_hash_updater.py @@ -2,18 +2,16 @@ from datetime import datetime, time from django.utils import timezone -from django.utils.timezone import get_default_timezone -from opening_hours.errors import ReservableTimeSpanClientNothingToDoError, ReservableTimeSpanClientValueError -from opening_hours.models import OriginHaukiResource -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_api_types import HaukiAPIResource -from opening_hours.utils.reservable_time_span_client import ReservableTimeSpanClient +from common.date_utils import DEFAULT_TIMEZONE +from tilavarauspalvelu.exceptions import ReservableTimeSpanClientNothingToDoError, ReservableTimeSpanClientValueError +from tilavarauspalvelu.models import OriginHaukiResource +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIResource +from tilavarauspalvelu.utils.opening_hours.reservable_time_span_client import ReservableTimeSpanClient logger = logging.getLogger(__name__) -DEFAULT_TIMEZONE = get_default_timezone() - class HaukiResourceHashUpdater: # List of resource ids that should be updated diff --git a/opening_hours/utils/reservable_time_span_client.py b/tilavarauspalvelu/utils/opening_hours/reservable_time_span_client.py similarity index 91% rename from opening_hours/utils/reservable_time_span_client.py rename to tilavarauspalvelu/utils/opening_hours/reservable_time_span_client.py index fd8d696f0..0b52c8c94 100644 --- a/opening_hours/utils/reservable_time_span_client.py +++ b/tilavarauspalvelu/utils/opening_hours/reservable_time_span_client.py @@ -5,20 +5,17 @@ from django.conf import settings from common.date_utils import local_date -from opening_hours.errors import ReservableTimeSpanClientNothingToDoError, ReservableTimeSpanClientValueError -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan -from opening_hours.utils.hauki_api_client import HaukiAPIClient -from opening_hours.utils.hauki_api_types import HaukiAPIOpeningHoursResponseItem -from opening_hours.utils.time_span_element import TimeSpanElement -from opening_hours.utils.time_span_element_utils import ( +from tilavarauspalvelu.constants import NEVER_ANY_OPENING_HOURS_HASH +from tilavarauspalvelu.exceptions import ReservableTimeSpanClientNothingToDoError, ReservableTimeSpanClientValueError +from tilavarauspalvelu.models import OriginHaukiResource, ReservableTimeSpan +from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIOpeningHoursResponseItem +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement +from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import ( merge_overlapping_time_span_elements, override_reservable_with_closed_time_spans, ) -# Hash value for when there are never any opening hours -# See https://github.com/City-of-Helsinki/hauki `hours.models.Resource._get_date_periods_as_hash` -NEVER_ANY_OPENING_HOURS_HASH = "d41d8cd98f00b204e9800998ecf8427e" # md5(b"").hexdigest() - class ReservableTimeSpanClient: DAYS_TO_FETCH = 730 # 2 years diff --git a/opening_hours/utils/summaries.py b/tilavarauspalvelu/utils/opening_hours/summaries.py similarity index 93% rename from opening_hours/utils/summaries.py rename to tilavarauspalvelu/utils/opening_hours/summaries.py index 729d81e14..c8f373b1a 100644 --- a/opening_hours/utils/summaries.py +++ b/tilavarauspalvelu/utils/opening_hours/summaries.py @@ -5,7 +5,7 @@ from django.db.models.functions import Coalesce from common.db import SubquerySum -from opening_hours.models import OriginHaukiResource, ReservableTimeSpan +from tilavarauspalvelu.models import OriginHaukiResource, ReservableTimeSpan def get_resources_total_hours_per_resource( diff --git a/opening_hours/utils/time_span_element.py b/tilavarauspalvelu/utils/opening_hours/time_span_element.py similarity index 98% rename from opening_hours/utils/time_span_element.py rename to tilavarauspalvelu/utils/opening_hours/time_span_element.py index a403153b7..ef8a0ce11 100644 --- a/opening_hours/utils/time_span_element.py +++ b/tilavarauspalvelu/utils/opening_hours/time_span_element.py @@ -4,8 +4,8 @@ from typing import Optional from common.date_utils import DEFAULT_TIMEZONE, combine, local_start_of_day -from opening_hours.enums import HaukiResourceState -from opening_hours.utils.hauki_api_types import HaukiAPIOpeningHoursResponseTime +from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIOpeningHoursResponseTime @dataclass(order=True, frozen=False) diff --git a/opening_hours/utils/time_span_element_utils.py b/tilavarauspalvelu/utils/opening_hours/time_span_element_utils.py similarity index 99% rename from opening_hours/utils/time_span_element_utils.py rename to tilavarauspalvelu/utils/opening_hours/time_span_element_utils.py index e20245603..d8c8eca46 100644 --- a/opening_hours/utils/time_span_element_utils.py +++ b/tilavarauspalvelu/utils/opening_hours/time_span_element_utils.py @@ -3,7 +3,7 @@ from itertools import chain from common.utils import with_indices -from opening_hours.utils.time_span_element import TimeSpanElement +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement def merge_overlapping_time_span_elements(*time_span_lists: Iterable[TimeSpanElement]) -> list[TimeSpanElement]: