diff --git a/tests/test_graphql_api/test_recurring_reservation/test_reschedule_series.py b/tests/test_graphql_api/test_recurring_reservation/test_reschedule_series.py index eff56c876a..7b17428a85 100644 --- a/tests/test_graphql_api/test_recurring_reservation/test_reschedule_series.py +++ b/tests/test_graphql_api/test_recurring_reservation/test_reschedule_series.py @@ -699,3 +699,59 @@ def test_recurring_reservations__reschedule_series__create_statistics__partial(g # But those statistics should be for different reservations. after = list(ReservationStatistic.objects.order_by("reservation").values_list("reservation", flat=True)) assert before != after + + +@freeze_time(local_datetime(year=2023, month=12, day=4, hour=10, minute=30)) # Monday +def test_recurring_reservations__reschedule_series__same_day_ongoing_reservation(graphql): + recurring_reservation = create_reservation_series() + + data = get_minimal_reschedule_data(recurring_reservation, beginTime="11:00:00") + + graphql.login_with_superuser() + response = graphql(RESCHEDULE_SERIES_MUTATION, input_data=data) + + assert response.has_errors is False + + # Series entries start at 11:00 instead of 10:00. + recurring_reservation.refresh_from_db() + assert recurring_reservation.begin_time == local_time(hour=11) + + reservations = list(recurring_reservation.reservations.order_by("begin").all()) + assert len(reservations) == 9 + assert reservations[0].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=10) + assert reservations[1].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[2].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[3].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[4].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[5].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[6].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[7].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[8].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + + +@freeze_time(local_datetime(year=2023, month=12, day=4, hour=8)) # Monday +def test_recurring_reservations__reschedule_series__same_day_future_reservation(graphql): + recurring_reservation = create_reservation_series() + + data = get_minimal_reschedule_data(recurring_reservation, beginTime="11:00:00") + + graphql.login_with_superuser() + response = graphql(RESCHEDULE_SERIES_MUTATION, input_data=data) + + assert response.has_errors is False + + # Series entries start at 11:00 instead of 10:00. + recurring_reservation.refresh_from_db() + assert recurring_reservation.begin_time == local_time(hour=11) + + reservations = list(recurring_reservation.reservations.order_by("begin").all()) + assert len(reservations) == 9 + assert reservations[0].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[1].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[2].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[3].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[4].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[5].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[6].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[7].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) + assert reservations[8].begin.astimezone(DEFAULT_TIMEZONE).timetz() == local_time(hour=11) diff --git a/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py b/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py index 2dbdf66589..b60114dccf 100644 --- a/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py +++ b/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py @@ -441,7 +441,7 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]: return data def save(self, **kwargs: Any) -> RecurringReservation: - skip_dates: list[datetime.date] = self.initial_data.get("skip_dates", []) + skip_dates: set[datetime.date] = set(self.initial_data.get("skip_dates", [])) buffer_time_before: datetime.timedelta | None = self.initial_data.get("buffer_time_before") buffer_time_after: datetime.timedelta | None = self.initial_data.get("buffer_time_after") @@ -472,35 +472,40 @@ def recreate_reservations( instance: RecurringReservation, buffer_time_before: datetime.timedelta | None, buffer_time_after: datetime.timedelta | None, - skip_dates: list[datetime.date], + skip_dates: set[datetime.date], ) -> list[Reservation]: now = local_datetime() today = now.date() - # New reservations can overlap with existing reservations in this series + # New reservations can overlap with existing reservations in this series, + # since the existing ones will be deleted. old_reservation_ids: list[int] = list(instance.reservations.values_list("pk", flat=True)) # Skip generating reservations for any dates where there is currently a non-confirmed reservation. # It's unlikely that the reserver will want or can have the same date even if the time is changed. # Any exceptions can be handled after the fact. - skip_dates += list( - instance.reservations.exclude( - state=ReservationStateChoice.CONFIRMED, - ).values_list("begin__date", flat=True) - ) + skip_dates |= set(instance.reservations.unconfirmed().values_list("begin__date", flat=True)) reservation_details = self.get_reservation_details(instance) reservation_details["buffer_time_before"] = buffer_time_before or datetime.timedelta() reservation_details["buffer_time_after"] = buffer_time_after or datetime.timedelta() - # Only recreate reservations from this moment onwards - if instance.begin_date < today: - skip_dates += [ + # If the series has already started: + if instance.begin_date <= today: + # Only create reservation to the future. + skip_dates |= { instance.begin_date + datetime.timedelta(days=delta) # for delta in range((today - instance.begin_date).days) - ] - if combine(today, instance.begin_time, tzinfo=DEFAULT_TIMEZONE) < now: - skip_dates.append(today) + } + + # If new reservation would already have started, don't create it. + if combine(today, instance.begin_time, tzinfo=DEFAULT_TIMEZONE) <= now: + skip_dates.add(today) + + # If series already has a reservation that is ongoing or in the past, don't create new one for today. + todays_reservation: Reservation | None = instance.reservations.filter(begin__date=today).first() + if todays_reservation is not None and todays_reservation.begin.astimezone(DEFAULT_TIMEZONE) <= now: + skip_dates.add(today) slots = instance.actions.pre_calculate_slots( check_buffers=True, diff --git a/tilavarauspalvelu/models/reservation/queryset.py b/tilavarauspalvelu/models/reservation/queryset.py index 927a0c6a76..7df1c13669 100644 --- a/tilavarauspalvelu/models/reservation/queryset.py +++ b/tilavarauspalvelu/models/reservation/queryset.py @@ -91,6 +91,9 @@ def future(self) -> Self: """Filter reservations have yet not begun.""" return self.going_to_occur().filter(begin__gt=local_datetime()) + def unconfirmed(self) -> Self: + return self.exclude(state=ReservationStateChoice.CONFIRMED) + def inactive(self, older_than_minutes: int) -> Self: """Filter 'draft' reservations, which are older than X minutes old, and can be assumed to be inactive.""" return self.filter(