Source code for feriennet.models.volunteer_cart

from itertools import groupby
from onegov.activity import OccasionNeed
from onegov.core.orm.sql import as_selectable_from_path
from onegov.core.utils import module_path
from onegov.feriennet import _
from sedate import overlaps
from sqlalchemy import select


from typing import Literal, TYPE_CHECKING
if TYPE_CHECKING:
    from collections.abc import Iterator, Sequence
    from datetime import datetime
    from onegov.core.browser_session import BrowserSession
    from onegov.core.types import JSON_ro, RenderData
    from onegov.feriennet.layout import DefaultLayout
    from onegov.feriennet.request import FeriennetRequest
    from sqlalchemy.orm import Query, Session
    from typing import NamedTuple
    from typing import Self
    from uuid import UUID

[docs] class CardItemRow(NamedTuple):
[docs] need_id: UUID
[docs] need_name: str
[docs] start: datetime
[docs] end: datetime
[docs] timezone: str
[docs] activity_title: str
[docs] occasion_id: UUID
[docs] class VolunteerCart: """ Stores items of the volunteer cart view in the browser session. Items stored this way may not overlap with existing items. We want to prevent volunteers from signing up for conflicting items. """ def __init__( self, session: 'Session', browser_session: 'BrowserSession' ) -> None:
[docs] self.session = session
[docs] self.browser_session = browser_session
@classmethod
[docs] def from_request(cls, request: 'FeriennetRequest') -> 'Self': return cls(request.session, request.browser_session)
[docs] def add(self, need_id: 'UUID') -> None: items: list[UUID] = self.browser_session.get('volunteer_cart', []) items.append(need_id) self.browser_session.volunteer_cart = items
[docs] def remove(self, need_id: 'UUID') -> None: self.browser_session.volunteer_cart = [ i for i in self.browser_session.get('volunteer_cart', ()) if i != need_id ]
[docs] def has(self, need_id: 'UUID') -> bool: return need_id in self.ids()
[docs] def ids(self) -> 'Sequence[UUID]': return self.browser_session.get('volunteer_cart', ())
[docs] def clear(self) -> None: if 'volunteer_cart' in self.browser_session: del self.browser_session.volunteer_cart
[docs] def card_items( self, need_id: 'UUID | None' = None ) -> 'Query[CardItemRow]': stmt = as_selectable_from_path( module_path('onegov.feriennet', 'queries/card_items.sql')) if need_id is None: need_ids = self.browser_session.get('volunteer_cart', ()) else: need_ids = (need_id, ) query = select(stmt.c).where(stmt.c.need_id.in_(need_ids)) return self.session.execute(query)
[docs] def overlaps(self, need_id: 'UUID') -> bool: need = self.session.query(OccasionNeed).filter_by(id=need_id).first() if not need: return False for maybe in self.card_items(need_id=need_id): for item in self.card_items(): if overlaps(maybe.start, maybe.end, item.start, item.end): return True return False
[docs] def for_frontend( self, layout: 'DefaultLayout', localize: bool = True ) -> 'Iterator[RenderData]': grouped = groupby(self.card_items(), key=lambda i: i.need_id) def date(record: 'CardItemRow') -> str: start = record.start end = record.end if localize: start = layout.to_timezone(start, record.timezone) end = layout.to_timezone(end, record.timezone) return layout.format_datetime_range(start, end) def remove(record: 'CardItemRow') -> str: return layout.csrf_protected_url( layout.request.link( VolunteerCartAction('remove', record.need_id))) for need_id, _records in grouped: records = tuple(_records) yield { 'need_id': need_id.hex, 'occasion_id': records[0].occasion_id.hex, 'remove': remove(records[0]), 'activity': records[0].activity_title, 'dates': [date(r) for r in records], 'need': records[0].need_name }
[docs] class VolunteerCartAction: """ Represents a single action for the VolunteerCart. """ def __init__( self, action: Literal['add', 'remove'], target: 'UUID' ):
[docs] self.action = action
[docs] self.target = target
[docs] def execute( self, request: 'FeriennetRequest', cart: VolunteerCart ) -> 'JSON_ro': if self.action == 'add': if cart.has(self.target): return { 'success': False, 'message': request.translate(_( 'This item is already in your list.' )) } if cart.overlaps(self.target): return { 'success': False, 'message': request.translate(_( 'Could not add item. It overlaps with ' 'another item in your list.' )) } cart.add(self.target) elif self.action == 'remove': cart.remove(self.target) else: pass return {'success': True}