Source code for feriennet.collections.match

from __future__ import annotations

from onegov.activity import Booking, Attendee, Occasion
from onegov.core.utils import toggle
from onegov.core.orm import as_selectable_from_path
from sqlalchemy import func
from sqlalchemy import select, and_
from onegov.core.utils import module_path
from statistics import mean


from typing import Literal, NamedTuple, TYPE_CHECKING
if TYPE_CHECKING:
    from collections.abc import Collection, Iterable
    from datetime import datetime
    from onegov.activity.models import BookingPeriod, BookingPeriodMeta
    from sqlalchemy.orm import Session
    from sqlalchemy.sql import Subquery
    from typing import Self
    from uuid import UUID

[docs] class OccasionByStateRow(NamedTuple):
[docs] state: OccasionState | None
[docs] occasion_id: UUID
[docs] activity_id: UUID
[docs] title: str
[docs] start: datetime
[docs] end: datetime
[docs] min_spots: int
[docs] max_spots: int
[docs] min_age: int
[docs] max_age: int
[docs] accepted_bookings: int
[docs] other_bookings: int
[docs] total_bookings: int
[docs] period_id: UUID
[docs] type OccasionState = Literal[ 'cancelled', 'overfull', 'empty', 'unoperable', 'operable', 'full' ]
[docs] class MatchCollection: def __init__( self, session: Session, period: BookingPeriod | BookingPeriodMeta, states: Collection[OccasionState] | None = None ) -> None:
[docs] self.session = session
[docs] self.period = period
[docs] self.states = set(states) if states else set()
@property
[docs] def period_id(self) -> UUID: return self.period.id
[docs] def for_period(self, period: BookingPeriod | BookingPeriodMeta) -> Self: return self.__class__(self.session, period)
[docs] def for_filter(self, state: OccasionState | None = None) -> Self: toggled = toggle(self.states, state) return self.__class__(self.session, self.period, toggled)
# FIXME: This might actually return Decimal in a query, in which case # we should fix the hybrid_method @property
[docs] def happiness(self) -> float: base = self.session.query(Attendee) q = base.with_entities(Attendee.happiness(self.period_id)) values = tuple(a.happiness for a in q if a.happiness is not None) if values: return mean(values) else: return 0
@property
[docs] def occasions_by_state(self) -> Subquery: return as_selectable_from_path( module_path('onegov.feriennet', 'queries/occasions_by_state.sql'))
@property
[docs] def operability(self) -> float: accepted = ( self.session.query(func.count(Booking.id).label('count')) .filter(Booking.occasion_id == Occasion.id) .filter(Booking.period_id == self.period_id) .filter(Booking.state == 'accepted') .subquery().lateral() ) o = self.session.query(Occasion.spots, accepted.c.count) o = o.filter(Occasion.period_id == self.period_id) bits = [ 1 if count >= spots.lower else 0 for spots, count in o ] if not bits: return 0 return sum(bits) / len(bits)
[docs] def include_in_output(self, occasion: OccasionByStateRow) -> bool: if not self.states: return True return occasion.state in self.states
@property
[docs] def occasions(self) -> Iterable[OccasionByStateRow]: columns = self.occasions_by_state.c query = select(*columns) if not self.states: query = query.where(columns.period_id == self.period_id) else: query = query.where(and_( columns.period_id == self.period_id, columns.state.in_(self.states) )) query = query.order_by(columns.title, columns.start) return self.session.execute(query).tuples()