from __future__ import annotations
from functools import cached_property
from onegov.activity import Activity, BookingPeriodCollection, Occasion
from onegov.activity import BookingCollection
from onegov.core.elements import Link, Confirm, Intercooler, Block
from onegov.core.elements import LinkGroup
from onegov.core.utils import linkify, paragraphify
from onegov.feriennet import _
from onegov.feriennet import security
from onegov.feriennet.collections import BillingCollection
from onegov.feriennet.collections import NotificationTemplateCollection
from onegov.feriennet.collections import OccasionAttendeeCollection
from onegov.feriennet.collections import VacationActivityCollection
from onegov.feriennet.const import OWNER_EDITABLE_STATES
from onegov.feriennet.models import InvoiceAction, VacationActivity
from onegov.town6.layout import DefaultLayout as BaseLayout
from onegov.town6.layout import UserLayout as TownUserLayout
from onegov.pay import PaymentProviderCollection
from onegov.ticket import TicketCollection
from typing import Any, NamedTuple, TYPE_CHECKING
from onegov.user.collections.user import UserCollection
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from markupsafe import Markup
from onegov.activity.models import (
Attendee, Booking, BookingPeriod, PublicationRequest)
from onegov.activity.collections import (
BookingPeriodInvoiceCollection, VolunteerCollection)
from onegov.core.elements import Trait
from onegov.feriennet.app import FeriennetApp
from onegov.feriennet.models import NotificationTemplate
from onegov.feriennet.request import FeriennetRequest
from onegov.org.models import Organisation
from onegov.ticket import Ticket
from onegov.user import User
[docs]
class DefaultLayout(BaseLayout):
[docs]
request: FeriennetRequest
@property
[docs]
def is_owner(self) -> bool:
if not self.request.current_username:
return False
return security.is_owner(self.request.current_username, self.model)
@property
[docs]
def is_editable(self) -> bool:
if self.request.is_admin:
return True
if not self.request.is_organiser:
return False
if isinstance(self.model, Activity):
return self.model.state in OWNER_EDITABLE_STATES
if isinstance(self.model, Occasion):
return self.model.activity.state in OWNER_EDITABLE_STATES
return True
[docs]
def offer_again_link(self, activity: VacationActivity, title: str) -> Link:
return Link(
text=title,
url=self.request.class_link(
VacationActivity,
{'name': activity.name},
name='offer-again'
),
traits=(
Confirm(
_(
'Do you really want to provide "${title}" again?',
mapping={'title': activity.title}
),
_('You will have to request publication again'),
_('Provide Again'),
_('Cancel')
),
Intercooler(
request_method='POST',
redirect_after=self.request.class_link(
VacationActivity, {'name': activity.name},
)
)
),
attrs={'class': 'offer-again'}
)
[docs]
def linkify(self, text: str | None) -> Markup: # type:ignore[override]
return linkify(text)
[docs]
def paragraphify(self, text: str) -> Markup:
return paragraphify(text)
[docs]
class VacationActivityCollectionLayout(DefaultLayout):
[docs]
model: VacationActivityCollection
if TYPE_CHECKING:
def __init__(
self,
model: VacationActivityCollection,
request: FeriennetRequest
) -> None: ...
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(_('Activities'), self.request.class_link(
VacationActivityCollection)),
]
@property
[docs]
def organiser_links(self) -> Iterator[Link | LinkGroup]:
if self.app.active_period:
yield Link(
text=_('Submit Activity'),
url=self.request.link(self.model, name='new'),
attrs={'class': 'new-activity'}
)
link_group = self.offer_again_links
if link_group is not None:
yield link_group
@property
[docs]
def offer_again_links(self) -> LinkGroup | None:
q = self.app.session().query(VacationActivity)
q = q.filter_by(username=self.request.current_username)
q = q.filter_by(state='archived')
q = q.with_entities(
VacationActivity.title,
VacationActivity.name,
)
q = q.order_by(VacationActivity.order)
activities = tuple(q)
if activities:
return LinkGroup(
_('Provide activity again'),
tuple(self.offer_again_link(a, a.title) for a in activities),
right_side=False,
classes=('provide-activity-again', )
)
return None
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup] | None:
if not self.request.is_organiser:
return None
return list(self.organiser_links)
[docs]
class BookingCollectionLayout(DefaultLayout):
[docs]
model: BookingCollection
def __init__(
self,
model: BookingCollection,
request: FeriennetRequest,
user: User | None = None
) -> None:
super().__init__(model, request)
if user is None:
user = request.current_user
assert user is not None
[docs]
def rega_link(
self,
attendee: Attendee | None,
period: BookingPeriod | None,
grouped_bookings: dict[Attendee, dict[str, list[Booking]]]
) -> str | None:
if not (period or attendee or grouped_bookings):
return None
if self.request.app.org.meta['locales'] == 'de_CH':
return ('https://www.rega.ch/partner/'
'das-pro-juventute-engagement-der-rega')
if self.request.app.org.meta['locales'] == 'it_CH':
return ('https://www.rega.ch/it/partner/'
'limpegno-pro-juventute-della-rega')
return ('https://www.rega.ch/fr/partenariats/'
'lengagement-de-la-rega-en-faveur-de-pro-juventute')
@cached_property
[docs]
def title(self) -> str:
wishlist_phase = (self.app.active_period
and self.app.active_period.wishlist_phase)
if self.user.username == self.request.current_username:
return wishlist_phase and _('Wishlist') or _('Bookings')
elif wishlist_phase:
return _('Wishlist of ${user}', mapping={
'user': self.user.title
})
else:
return _('Bookings of ${user}', mapping={
'user': self.user.title
})
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(self.title, self.request.link(self.model))
]
[docs]
class GroupInviteLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
wishlist_phase = (self.app.active_period
and self.app.active_period.wishlist_phase)
if self.request.is_logged_in:
return [
Link(_('Homepage'), self.homepage_url),
Link(
wishlist_phase and _('Wishlist') or _('Bookings'),
self.request.class_link(BookingCollection)
),
Link(_('Group'), '#')
]
else:
return [
Link(_('Homepage'), self.homepage_url),
Link(_('Group'), '#')
]
[docs]
class VacationActivityLayout(DefaultLayout):
[docs]
model: VacationActivity
if TYPE_CHECKING:
def __init__(
self,
model: VacationActivity,
request: FeriennetRequest
) -> None: ...
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(_('Activities'), self.request.class_link(
VacationActivityCollection)),
Link(self.model.title, self.request.link(self.model))
]
@cached_property
[docs]
def latest_request(self) -> PublicationRequest | None:
return self.model.latest_request
@cached_property
[docs]
def ticket(self) -> Ticket | None:
if self.latest_request:
tickets = TicketCollection(self.request.session)
return tickets.by_handler_id(self.latest_request.id.hex)
return None
@cached_property
[docs]
def attendees(self) -> OccasionAttendeeCollection | None:
if self.request.app.default_period:
return OccasionAttendeeCollection(
self.request.session,
self.request.app.default_period,
self.model
)
return None
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup]:
links: list[Link | LinkGroup] = []
period = self.request.app.active_period
if self.request.is_admin or self.is_owner:
if self.model.state == 'archived' and period:
links.append(
self.offer_again_link(self.model, _('Provide Again')))
if self.is_editable:
if self.model.state == 'preview':
if not period:
links.append(Link(
text=_('Request Publication'),
url='#',
attrs={'class': 'request-publication'},
traits=(
Block(
_(
'There is currently no active period. '
'Please retry once a period has been '
'activated.'
),
no=_('Cancel')
),
)
))
elif self.model.has_occasion_in_period(period):
links.append(Link(
text=_('Request Publication'),
url=self.request.link(self.model, name='propose'),
attrs={'class': 'request-publication'},
traits=(
Confirm(
_(
'Do you really want to request '
'publication?'
),
_('This cannot be undone.'),
_('Request Publication')
),
Intercooler(
request_method='POST',
redirect_after=self.request.link(self.model)
)
)
))
else:
links.append(Link(
text=_('Request Publication'),
url='#',
attrs={'class': 'request-publication'},
traits=(
Block(
_(
'Please add at least one occasion '
'before requesting publication.'
),
no=_('Cancel')
),
)
))
if not self.model.publication_requests:
links.append(Link(
text=_('Discard'),
url=self.csrf_protected_url(
self.request.link(self.model)
),
attrs={'class': 'delete-link'},
traits=(
Confirm(_(
'Do you really want to discard "${title}"?',
mapping={'title': self.model.title}
), _(
'This cannot be undone.'
), _(
'Discard Activity'
), _(
'Cancel')
),
Intercooler(
request_method='DELETE',
redirect_after=self.request.class_link(
VacationActivityCollection
)
)
)
))
links.append(Link(
text=_('Edit'),
url=self.request.link(self.model, name='edit'),
attrs={'class': 'edit-link'}
))
if not self.request.app.periods:
links.append(Link(
text=_('New Occasion'),
url='#',
attrs={'class': 'new-occasion'},
traits=(
Block(
_('Occasions cannot be created yet'),
_(
'There are no periods defined yet. At least '
'one period needs to be defined before '
'occasions can be created.'
),
_('Cancel')
)
)
))
else:
links.append(Link(
text=_('New Occasion'),
url=self.request.link(self.model, 'new-occasion'),
attrs={'class': 'new-occasion'}
))
if self.request.is_admin or self.is_owner:
if self.attendees:
links.append(Link(
text=_('Attendees'),
url=self.request.link(self.attendees),
attrs={'class': 'show-attendees'}
))
if self.request.is_admin:
if self.model.state != 'preview' and self.ticket:
links.append(Link(
text=_('Show Ticket'),
url=self.request.link(self.ticket),
attrs={'class': 'show-ticket'}
))
return links
[docs]
class PeriodCollectionLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(_('Manage Periods'), '#')
]
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup]:
return [
Link(
_('New Period'),
self.request.class_link(BookingPeriodCollection, name='new'),
attrs={'class': 'new-period'}
),
]
[docs]
class MatchCollectionLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(_('Matches'), '#')
]
[docs]
class BillingCollectionLayout(DefaultLayout):
[docs]
model: BillingCollection
if TYPE_CHECKING:
def __init__(
self,
model: BillingCollection,
request: FeriennetRequest
) -> None: ...
[docs]
class FamilyRow(NamedTuple):
[docs]
count: int # type:ignore[assignment]
[docs]
has_online_payments: bool
@property
[docs]
def families(self) -> Iterator[FamilyRow]:
yield from self.app.session().execute("""
SELECT
text
|| ' ('
|| replace(avg(unit * quantity)::money::text, '$', '')
|| ' CHF)'
AS text
,
MIN(id::text) AS item,
COUNT(*) AS count,
family IN (
SELECT DISTINCT(family)
FROM invoice_items
WHERE source IS NOT NULL and source != 'xml'
) AS has_online_payments
FROM invoice_items
WHERE family IS NOT NULL
GROUP BY family, text
ORDER BY text
""")
@property
[docs]
def family_removal_links(self) -> Iterator[Link]:
attrs = {
'class': ('remove-manual', 'extend-to-family')
}
for record in self.families:
text = _('Delete "${text}"', mapping={
'text': record.text,
})
url = self.csrf_protected_url(
self.request.class_link(InvoiceAction, {
'id': record.item,
'action': 'remove-manual',
'extend_to': 'family'
})
)
traits: Sequence[Trait]
if record.has_online_payments:
traits = (
Block(
_(
'This booking cannot be removed, at least one '
'booking has been paid online.'
),
_(
'You may remove the bookings manually one by one.'
),
_('Cancel')
),
)
else:
traits = (
Confirm(
_('Do you really want to remove "${text}"?', mapping={
'text': record.text
}),
_('${count} bookings will be removed', mapping={
'count': record.count
}),
_('Remove ${count} bookings', mapping={
'count': record.count
}),
_('Cancel')
),
Intercooler(request_method='POST')
)
yield Link(text=text, url=url, attrs=attrs, traits=traits)
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(_('Billing'), '#')
]
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup]:
return [
Link(
_('Import Bank Statement'),
self.request.link(self.model, 'import'),
attrs={'class': 'import'}
),
Link(
_('Synchronise Online Payments'),
self.request.return_here(
self.request.class_link(
PaymentProviderCollection, name='sync')),
attrs={'class': 'sync'},
),
LinkGroup(
title=_('Accounting'),
links=[
Link(
text=_('Manual Booking'),
url=self.request.link(
self.model,
name='booking'
),
attrs={'class': 'new-booking'},
traits=(
Block(_(
'Manual bookings can only be added '
'once the billing has been confirmed.'
), no=_('Cancel')),
) if not self.model.period.finalized else ()
),
*self.family_removal_links
]
)
]
[docs]
class OnlinePaymentsLayout(DefaultLayout):
def __init__(
self,
model: Any,
request: FeriennetRequest,
title: str
) -> None:
super().__init__(model, request)
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup]:
return [
Link(
_('Synchronise Online Payments'),
self.request.return_here(
self.request.class_link(
PaymentProviderCollection, name='sync')),
attrs={'class': 'sync'},
),
]
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(
_('Billing'),
self.request.class_link(BillingCollection)
),
Link(self.title, '#')
]
[docs]
class BillingCollectionImportLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(_('Billing'), self.request.link(self.model)),
Link(_('Import Bank Statement'), '#')
]
[docs]
class BillingCollectionManualBookingLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(_('Billing'), self.request.link(self.model)),
Link(_('Manual Booking'), '#')
]
[docs]
class BillingCollectionPaymentWithDateLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(_('Billing'), self.request.link(self.model)),
Link(_('Payment with date'), '#')
]
[docs]
class InvoiceLayout(DefaultLayout):
def __init__(
self,
model: Any,
request: FeriennetRequest,
title: str
) -> None:
super().__init__(model, request)
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(self.title, '#')
]
[docs]
class DonationLayout(DefaultLayout):
def __init__(
self,
model: BookingPeriodInvoiceCollection,
request: FeriennetRequest,
title: str
) -> None:
super().__init__(model, request)
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(_('Invoices'), self.request.link(self.model)),
Link(_('Donation'), self.title)
]
[docs]
class OccasionAttendeeLayout(DefaultLayout):
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(
self.model.activity.title,
self.request.link(self.model.activity)
),
Link(_('Attendees'), '#')
]
[docs]
class NotificationTemplateCollectionLayout(DefaultLayout):
[docs]
model: NotificationTemplateCollection
def __init__(
self,
model: NotificationTemplateCollection,
request: FeriennetRequest,
subtitle: str | None = None
) -> None:
super().__init__(model, request)
[docs]
self.subtitle = subtitle
self.include_editor()
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
links = [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(
_('Notification Templates'),
self.request.class_link(NotificationTemplateCollection)
)
]
if self.subtitle:
links.append(Link(self.subtitle, '#'))
return links
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup] | None:
if not self.subtitle:
return [
Link(
_('New Notification Template'),
self.request.link(self.model, 'new'),
attrs={'class': 'new-notification'}
),
]
return None
[docs]
class NotificationTemplateLayout(DefaultLayout):
[docs]
model: NotificationTemplate
def __init__(
self,
model: NotificationTemplate,
request: FeriennetRequest,
subtitle: str | None = None
) -> None:
super().__init__(model, request)
[docs]
self.subtitle = subtitle
self.include_editor()
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
links = [
Link(_('Homepage'), self.homepage_url),
Link(
_('Activities'),
self.request.class_link(VacationActivityCollection)
),
Link(
_('Notification Templates'),
self.request.class_link(NotificationTemplateCollection)
),
Link(
self.model.subject,
self.request.link(self.model)
)
]
if self.subtitle:
links.append(Link(self.subtitle, '#'))
return links
[docs]
class VolunteerLayout(DefaultLayout):
[docs]
model: VolunteerCollection
if TYPE_CHECKING:
def __init__(
self,
model: VolunteerCollection,
request: FeriennetRequest
) -> None: ...
@cached_property
[docs]
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(_('Volunteers'), self.request.link(self.model))
]
[docs]
class HomepageLayout(DefaultLayout):
[docs]
model: Organisation
if TYPE_CHECKING:
def __init__(
self,
model: Organisation,
request: FeriennetRequest
) -> None: ...
@property
[docs]
def editbar_links(self) -> list[Link | LinkGroup] | None:
if self.request.is_manager:
return [
Link(
_('Sort'),
self.request.link(self.model, 'sort'),
attrs={'class': ('sort-link')}
)
]
return None
[docs]
class UserLayout(TownUserLayout):
if TYPE_CHECKING:
def __init__(
self,
model: User,
request: FeriennetRequest
) -> None: ...
@cached_property
[docs]
def editbar_links(self) -> list[Link | LinkGroup] | None:
links: list[Link | LinkGroup] = []
if self.request.is_admin and not self.model.source:
links.append(
Link(
text=_('Edit'),
url=self.request.link(self.model, 'edit'),
attrs={'class': 'edit-link'}
)
)
if self.model.role != 'admin':
links.append(Link(
text=_('Delete'),
url=self.csrf_protected_url(
self.request.link(self.model)
),
attrs={'class': 'delete-link'},
traits=(
Confirm(
_('Do you really want to delete this user?'),
_('This cannot be undone.'),
_('Delete user'),
_('Cancel')
),
Intercooler(
request_method='DELETE',
redirect_after=self.request.class_link(
UserCollection)
)
)
))
return links
return None