import sedate
import random
from datetime import date, timedelta
from itertools import groupby
from onegov.activity import Activity
from onegov.activity import Booking
from onegov.activity import Occasion
from onegov.activity import OccasionCollection
from onegov.activity import Period
from onegov.activity.models import ACTIVITY_STATES, DAYS
from onegov.core.elements import Link, Confirm, Intercooler
from onegov.core.security import Personal
from onegov.core.security import Private
from onegov.core.security import Public
from onegov.core.security import Secret
from onegov.core.utils import normalize_for_url
from onegov.feriennet import _
from onegov.feriennet import FeriennetApp
from onegov.feriennet.collections import VacationActivityCollection
from onegov.feriennet.forms import VacationActivityForm
from onegov.feriennet.layout import VacationActivityCollectionLayout
from onegov.feriennet.layout import VacationActivityFormLayout
from onegov.feriennet.layout import VacationActivityLayout
from onegov.feriennet.models import ActivityMessage
from onegov.feriennet.models import VacationActivity
from onegov.feriennet.models import VolunteerCart
from onegov.feriennet.models import VolunteerCartAction
from onegov.org.mail import send_ticket_mail
from onegov.org.models import TicketMessage
from onegov.ticket import TicketCollection
from purl import URL
from re import search
from sedate import dtrange, overlaps
from sqlalchemy import desc
from sqlalchemy.orm import contains_eager
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import undefer
from webob import exc
from typing import Literal, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Sequence
from decimal import Decimal
from onegov.activity.models import OccasionDate
from onegov.activity.types import BoundedIntegerRange
from onegov.core.types import JSON_ro, RenderData
from onegov.feriennet.request import FeriennetRequest
from onegov.ticket import Ticket
from sqlalchemy.orm import Session
from webob import Response
[docs]
ACTIVITY_STATE_TRANSLATIONS = {
'preview': _('Preview'),
'proposed': _('Proposed'),
'accepted': _('Published'), # users like the term 'Published' better
'archived': _('Archived')
}
[docs]
WEEKDAYS = (
_('Mo'),
_('Tu'),
_('We'),
_('Th'),
_('Fr'),
_('Sa'),
_('Su')
)
[docs]
def occasions_by_period(
session: 'Session',
activity: Activity,
show_inactive: bool,
show_archived: bool
) -> tuple[tuple[str, tuple[Occasion, ...]], ...]:
query = OccasionCollection(session).query()
query = query.filter(Occasion.activity_id == activity.id)
query = query.join(Occasion.period)
query = query.options(contains_eager(Occasion.period))
if not show_inactive:
query = query.filter(Period.active == True)
if not show_archived:
query = query.filter(Period.archived == False)
query = query.order_by(
desc(Period.active),
Period.execution_start,
Occasion.order)
return tuple(
(title, tuple(occasions)) for title, occasions in
groupby(query, key=lambda o: o.period.title)
)
[docs]
def filter_link(
text: str,
active: bool,
url: str,
rounded: bool = False
) -> Link:
return Link(text=text, active=active, url=url, rounded=rounded, attrs={
'ic-get-from': url
})
[docs]
def filter_timelines(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> list[Link]:
links = [
filter_link(
text=request.translate(_('Elapsed')),
active='past' in activity.filter.timelines,
url=request.link(activity.for_filter(timeline='past'))
),
filter_link(
text=request.translate(_('Now')),
active='now' in activity.filter.timelines,
url=request.link(activity.for_filter(timeline='now'))
),
filter_link(
text=request.translate(_('Scheduled')),
active='future' in activity.filter.timelines,
url=request.link(activity.for_filter(timeline='future'))
),
]
if request.is_organiser:
links.insert(0, filter_link(
text=request.translate(_('Without')),
active='undated' in activity.filter.timelines,
url=request.link(activity.for_filter(timeline='undated'))
))
return links
[docs]
def filter_durations(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
return tuple(
filter_link(
text=request.translate(text),
active=duration in activity.filter.durations,
url=request.link(activity.for_filter(duration=duration))
) for text, duration in (
(_('Half day'), DAYS.half),
(_('Full day'), DAYS.full),
(_('Multiple days'), DAYS.many),
)
)
[docs]
def filter_ages(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
ages = activity.available_ages()
if not ages:
return ()
def age_filters() -> 'Iterator[tuple[str, tuple[int, int]]]':
for age in range(*ages):
if age < 16:
yield str(age), (age, age)
else:
yield '16+', (16, 99)
break
return tuple(
filter_link(
text=request.translate(text),
active=activity.filter.contains_age_range(age_range),
url=request.link(activity.for_filter(age_range=age_range))
) for text, age_range in age_filters()
)
[docs]
def filter_price_range(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
return tuple(
filter_link(
text=request.translate(text),
active=activity.filter.contains_price_range(price_range),
url=request.link(activity.for_filter(price_range=price_range))
) for text, price_range in (
(_('Free of Charge'), (0, 0)),
(_('Up to 25 CHF'), (1, 25)),
(_('Up to 50 CHF'), (26, 50)),
(_('Up to 100 CHF'), (51, 100)),
(_('More than 100 CHF'), (101, 100000)),
)
)
[docs]
def filter_weeks(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
# FIXME: format_date should be available on the request, so we don't
# need to create dummy layouts in order to use it.
layout = VacationActivityCollectionLayout(activity, request)
return tuple(
filter_link(
text='{} - {}'.format(
layout.format_date(daterange[0], 'date'),
layout.format_date(daterange[1], 'date')
),
active=daterange in activity.filter.dateranges,
url=request.link(activity.for_filter(daterange=daterange))
) for nth, daterange in enumerate(
activity.available_weeks(request.app.active_period),
start=1
)
)
[docs]
def filter_weekdays(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
return tuple(
filter_link(
text=WEEKDAYS[weekday],
active=weekday in activity.filter.weekdays,
url=request.link(activity.for_filter(weekday=weekday))
) for weekday in range(7)
)
[docs]
def filter_available(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
# NOTE: We're helping out mypy's inference here, since it won't
# infer the second tuple element as a literal
availabilities: tuple[
tuple[str, Literal['none', 'few', 'many']],
...
] = (
(_('None'), 'none'),
(_('Few'), 'few'),
(_('Many'), 'many'),
)
return tuple(
filter_link(
text=request.translate(text),
active=available in activity.filter.available,
url=request.link(activity.for_filter(available=available))
) for text, available in availabilities
)
[docs]
def filter_municipalities(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> list[Link]:
links = [
filter_link(
text=municipality,
active=municipality in activity.filter.municipalities,
url=request.link(activity.for_filter(municipality=municipality))
) for municipality in activity.used_municipalities
]
links.sort(key=lambda l: normalize_for_url(l.text)) # type:ignore
return links
[docs]
def filter_periods(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> list[Link]:
links = [
filter_link(
text=period.title,
active=period.id in activity.filter.period_ids,
url=request.link(activity.for_filter(period_id=period.id))
) for period in request.app.periods if period
]
links.sort(key=lambda l: l.text) # type:ignore
return links
[docs]
def filter_own(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
assert request.current_username is not None
return (
filter_link(
text=request.translate(_('Own')),
active=request.current_username in activity.filter.owners,
url=request.link(
activity.for_filter(owner=request.current_username)
)
),
)
[docs]
def filter_states(
activity: VacationActivityCollection,
request: 'FeriennetRequest'
) -> tuple[Link, ...]:
return tuple(
filter_link(
text=ACTIVITY_STATE_TRANSLATIONS[state],
active=state in activity.filter.states,
url=request.link(activity.for_filter(state=state))
) for state in ACTIVITY_STATES
)
[docs]
def period_bound_occasions(
activity: Activity,
request: 'FeriennetRequest'
) -> list[Occasion]:
active_period = request.app.active_period
if not active_period:
return []
return [o for o in activity.occasions if o.period_id == active_period.id]
[docs]
def activity_ages(
activity: Activity,
request: 'FeriennetRequest'
) -> tuple['BoundedIntegerRange', ...]:
return tuple(o.age for o in period_bound_occasions(activity, request))
[docs]
def activity_spots(
activity: Activity,
request: 'FeriennetRequest'
) -> int:
if not request.app.active_period:
return 0
if not request.app.active_period.confirmed:
return sum(o.max_spots for o in period_bound_occasions(
activity, request))
return sum(o.available_spots for o in period_bound_occasions(
activity, request))
[docs]
def activity_min_cost(
activity: Activity,
request: 'FeriennetRequest'
) -> 'Decimal | None':
occasions = period_bound_occasions(activity, request)
if not occasions:
return None
return min(o.total_cost for o in occasions)
[docs]
def activity_max_cost(
activity: Activity,
request: 'FeriennetRequest'
) -> 'Decimal | None':
occasions = period_bound_occasions(activity, request)
if not occasions:
return None
return max(o.total_cost for o in occasions)
[docs]
def is_filtered(filters: dict[str, 'Sequence[Link]']) -> bool:
for links in filters.values():
for link in links:
if link.active:
return True
return False
[docs]
def adjust_filter_path(
filters: dict[str, 'Sequence[Link]'],
suffix: str
) -> None:
for links in filters.values():
for link in links:
link.attrs['href'] = link.attrs['ic-get-from'] = URL(
link.attrs['href']).add_path_segment(suffix).as_string()
[docs]
def exclude_filtered_dates(
activities: VacationActivityCollection,
dates: 'Iterable[OccasionDate]'
) -> list['OccasionDate']:
today = date.today()
return [
dt
for dt in dates
# only include future date ranges
if dt.start.date() > today
# .. that overlap with the selected date ranges
# unless we didn't select any date ranges
if not activities.filter.dateranges or any(
overlaps(dt.start.date(), dt.end.date(), s, e)
for s, e in activities.filter.dateranges
)
# .. and don't contain a weekday that wasn't selected
# unless we didn't select any weekdays
if not activities.filter.weekdays or all(
day.weekday() in activities.filter.weekdays
# NOTE: This is technically quite inefficient for date ranges
# that are longer than a week, but realistically most
# activities will be much shorter than that.
for day in dtrange(dt.start, dt.end)
)
]
@FeriennetApp.html(
model=VacationActivityCollection,
template='activities.pt',
permission=Public)
[docs]
def view_activities(
self: VacationActivityCollection,
request: 'FeriennetRequest'
) -> 'RenderData':
active_period = request.app.active_period
show_activities = bool(active_period or request.is_organiser)
layout = VacationActivityCollectionLayout(self, request)
filters: dict[str, Sequence[Link]] = {}
if show_activities:
filters['timelines'] = filter_timelines(self, request)
filters['tags'] = filter_tags(self, request)
filters['durations'] = filter_durations(self, request)
filters['ages'] = filter_ages(self, request)
filters['price_range'] = filter_price_range(self, request)
if active_period:
filters['weeks'] = filter_weeks(self, request)
filters['weekdays'] = filter_weekdays(self, request)
filters['available'] = filter_available(self, request)
filters['municipalities'] = filter_municipalities(self, request)
if request.is_organiser:
if request.app.periods:
filters['periods'] = filter_periods(self, request)
filters['own'] = filter_own(self, request)
filters['states'] = filter_states(self, request)
filters = {k: v for k, v in filters.items() if v}
all_sponsors = layout.app.banners(request)
main_sponsor = all_sponsors[0]
sponsors = all_sponsors[1:len(all_sponsors)]
activities = list(self.batch) if show_activities else []
return {
'activities': activities,
'main_sponsor': main_sponsor,
'sponsors': sponsors,
'random': random,
'layout': layout,
'title': _('Activities'),
'filters': filters,
'filtered': is_filtered(filters),
'period': active_period,
'activity_ages': activity_ages,
'activity_min_cost': activity_min_cost,
'activity_spots': activity_spots,
'current_location': request.link(
self.by_page_range((0, self.pages[-1])))
}
@FeriennetApp.json(
model=VacationActivityCollection,
name='json',
permission=Public
)
[docs]
def view_activities_as_json(
self: VacationActivityCollection,
request: 'FeriennetRequest'
) -> 'JSON_ro':
self.filter.states = {'accepted'}
active_period = request.app.active_period
def image(activity: VacationActivity) -> 'JSON_ro':
url = (activity.meta or {}).get('thumbnail')
return {
'thumbnail': url,
'full': url.replace('/thumbnail', '') if url else None
}
def age(activity: VacationActivity) -> 'JSON_ro':
ages = activity_ages(activity, request)
min_age = min(age.lower for age in ages) if ages else None
max_age = max(age.upper - 1 for age in ages) if ages else None
return {'min': min_age, 'max': max_age}
def cost(activity: VacationActivity) -> 'JSON_ro':
min_cost = activity_min_cost(activity, request)
max_cost = activity_max_cost(activity, request)
return {
'min': float(min_cost) if min_cost is not None else 0.0,
'max': float(max_cost) if max_cost is not None else 0.0
}
def dates(activity: VacationActivity) -> 'JSON_ro':
occasion_dates = []
for occasion in activity.occasions:
for occasion_date in occasion.dates:
start = occasion_date.localized_start
end = occasion_date.localized_end
occasion_dates.append({
'start_date': start.date().isoformat(),
'start_time': start.time().isoformat(),
'end_date': end.date().isoformat(),
'end_time': end.time().isoformat(),
})
return occasion_dates
def zip_code(activity: VacationActivity) -> int | None:
match = search(r'(\d){4}', activity.location or '')
return int(match.group()) if match else None
def coordinates(activity: VacationActivity) -> 'JSON_ro':
lat = activity.coordinates.lat if activity.coordinates else None
lon = activity.coordinates.lon if activity.coordinates else None
return {'lat': lat, 'lon': lon}
def tags(activity: VacationActivity) -> 'JSON_ro':
period = request.app.active_period
period_id = period.id if period else None
durations = sum({
o.duration
for o in activity.occasions
if o.period_id == period_id and o.duration is not None
})
return activity.ordered_tags(request, durations)
provider = request.app.org.title
if active_period:
wish_start = None
wish_end = None
if active_period.confirmable:
wish_start = active_period.prebooking_start.isoformat()
wish_end = active_period.prebooking_end.isoformat()
return {
'period_name': active_period.title,
'wish_phase_start': wish_start,
'wish_phase_end': wish_end,
'booking_phase_start': active_period.booking_start.isoformat(),
'booking_phase_end': active_period.booking_end.isoformat(),
'deadline_days': active_period.deadline_days,
'activities': [
{
'provider': provider,
'url': request.link(activity),
'title': activity.title,
'lead': (activity.meta or {}).get('lead', ''),
'image': image(activity),
'age': age(activity),
'cost': cost(activity),
'spots': activity_spots(activity, request),
'dates': dates(activity),
'location': activity.location,
'zip_code': zip_code(activity),
'coordinate': coordinates(activity),
'tags': tags(activity),
} for activity in self.query().options(
joinedload(Activity.occasions),
undefer(Activity.content))
]
}
else:
return {}
@FeriennetApp.html(
model=VacationActivityCollection,
template='activities-for-volunteers.pt',
permission=Public,
name='volunteer')
[docs]
def view_activities_for_volunteers(
self: VacationActivityCollection,
request: 'FeriennetRequest'
) -> 'RenderData':
if not request.app.show_volunteers(request):
raise exc.HTTPForbidden()
active_period = request.app.active_period
show_activities = bool(active_period or request.is_organiser)
layout = VacationActivityCollectionLayout(self, request)
layout.breadcrumbs[-1].text = _('Join as a Volunteer')
# always limit to activities seeking volunteers
self.filter.volunteers = {True}
# include javascript part
request.include('volunteer-cart')
filters: dict[str, Sequence[Link]] = {}
if show_activities:
filters['tags'] = filter_tags(self, request)
filters['durations'] = filter_durations(self, request)
if active_period:
filters['weeks'] = filter_weeks(self, request)
self.filter.period_ids = {active_period.id}
filters['weekdays'] = filter_weekdays(self, request)
filters['municipalities'] = filter_municipalities(self, request)
filters = {k: v for k, v in filters.items() if v}
adjust_filter_path(filters, suffix='volunteer')
return {
'activities': self.batch if show_activities else None,
'layout': layout,
'title': _('Join as a Volunteer'),
'filters': filters,
'filtered': is_filtered(filters),
'period': active_period,
'activity_ages': activity_ages,
'activity_min_cost': activity_min_cost,
'activity_spots': activity_spots,
'exclude_filtered_dates': exclude_filtered_dates,
'cart_url': request.class_link(VolunteerCart),
'cart_submit_url': request.class_link(VolunteerCart, name='submit'),
'cart_action_url': request.class_link(VolunteerCartAction, {
'action': 'action',
'target': 'target',
}),
'current_location': request.link(
self.by_page_range((0, self.pages[-1])), name='volunteer')
}
@FeriennetApp.html(
model=VacationActivity,
template='activity.pt',
permission=Public)
[docs]
def view_activity(
self: VacationActivity,
request: 'FeriennetRequest'
) -> 'RenderData':
session = request.session
layout = VacationActivityLayout(self, request)
occasion_ids = {o.id for o in self.occasions}
occasion_ids_with_bookings = occasion_ids and {
b.occasion_id for b in session.query(Booking)
.with_entities(Booking.occasion_id)
.filter(Booking.occasion_id.in_(occasion_ids))
} or set()
def occasion_links(o: Occasion) -> 'Iterator[Link]':
if not o.period.archived and (o.period.active or request.is_admin):
yield Link(text=_('Edit'), url=request.link(o, name='edit'))
yield Link(text=_('Clone'), url=request.link(o, name='clone'))
title = layout.format_datetime_range(
o.dates[0].localized_start,
o.dates[0].localized_end
)
can_cancel = not o.cancelled and (
request.is_admin or not o.period.finalized
)
if o.cancelled and not o.period.finalized:
yield Link(
text=_('Reinstate'),
url=layout.csrf_protected_url(
request.link(o, name='reinstate')
),
traits=(
Confirm(
_(
'Do you really want to reinstate "${title}"?',
mapping={'title': title}
),
_('Previous attendees need to re-apply'),
_('Reinstate Occasion'),
_('Cancel')
),
Intercooler(
request_method='POST',
redirect_after=request.link(self)
)
)
)
elif o.id in occasion_ids_with_bookings and can_cancel:
yield Link(
text=_('Rescind'),
url=layout.csrf_protected_url(request.link(o, name='cancel')),
traits=(
Confirm(
_(
'Do you really want to rescind "${title}"?',
mapping={'title': title}
),
_(
'${count} already accepted bookings will '
'be cancelled', mapping={'count': o.attendee_count}
),
_(
'Rescind Occasion'
),
_(
'Cancel'
)
),
Intercooler(
request_method='POST',
redirect_after=request.link(self)
)
)
)
elif o.id not in occasion_ids_with_bookings:
yield Link(
text=_('Delete'),
url=layout.csrf_protected_url(request.link(o)),
traits=(
Confirm(
_('Do you really want to delete "${title}"?', mapping={
'title': title
}),
_(
'There are no accepted bookings associated with '
'this occasion, though there might be '
'cancelled/blocked bookings which will be deleted.'
),
_('Delete Occasion'),
_('Cancel')
),
Intercooler(
request_method='DELETE',
redirect_after=request.link(self)
)
)
)
def show_enroll(occasion: Occasion) -> bool:
if self.state != 'accepted':
return False
if not occasion.period.active:
return False
if occasion.cancelled:
return False
if occasion.full and occasion.period.phase != 'wishlist':
return False
# the rest of the restrictions only apply to non-admins
if request.is_admin:
return True
if occasion.period.finalized and not occasion.period.book_finalized:
return False
acceptable_phases: tuple[str, ...]
if occasion.period.finalized and occasion.period.book_finalized:
acceptable_phases = ('wishlist', 'booking', 'execution', 'payment')
else:
acceptable_phases = ('wishlist', 'booking', 'execution')
if occasion.period.phase not in acceptable_phases:
return False
if occasion.is_past_deadline(sedate.utcnow()):
return False
if (
occasion.period.wishlist_phase
and occasion.period.is_prebooking_in_past
):
return False
return True
return {
'layout': layout,
'title': self.title,
'activity': self,
'show_enroll': show_enroll,
'occasion_links': occasion_links,
'occasions_by_period': occasions_by_period(
session=session,
activity=self,
show_inactive=request.is_organiser,
show_archived=request.is_admin or (
request.is_organiser
and self.username == request.current_username
)
),
}
@FeriennetApp.form(
model=VacationActivityCollection,
template='form.pt',
form=get_activity_form_class,
permission=Private,
name='new')
[docs]
def new_activity(
self: VacationActivityCollection,
request: 'FeriennetRequest',
form: VacationActivityForm
) -> 'RenderData | Response':
if form.submitted(request):
assert form.title.data is not None
assert request.current_username is not None
activity = self.add(
title=form.title.data,
username=request.current_username)
form.populate_obj(activity)
return request.redirect(request.link(activity))
layout = VacationActivityFormLayout(self, request, _('New Activity'))
layout.edit_mode = True
return {
'layout': layout,
'title': _('New Activity'),
'form': form
}
@FeriennetApp.form(
model=VacationActivity,
template='form.pt',
form=get_activity_form_class,
permission=Private,
name='edit')
[docs]
def edit_activity(
self: VacationActivity,
request: 'FeriennetRequest',
form: VacationActivityForm
) -> 'RenderData | Response':
if form.submitted(request):
old_username = self.username
form.populate_obj(self)
new_username = self.username
if old_username != new_username:
# if there is already a ticket..
ticket = relevant_ticket(self, request)
if ticket:
# ..note the change
ActivityMessage.create(
ticket, request, 'reassign',
old_username=old_username,
new_username=new_username
)
request.success(_('Your changes were saved'))
return request.redirect(request.link(self))
elif not request.POST:
form.process(obj=self)
layout = VacationActivityFormLayout(self, request, _('Edit Activity'))
layout.edit_mode = True
return {
'layout': layout,
'title': self.title,
'form': form
}
@FeriennetApp.view(
model=VacationActivity,
permission=Private,
request_method='DELETE')
[docs]
def discard_activity(
self: VacationActivity,
request: 'FeriennetRequest'
) -> None:
request.assert_valid_csrf_token()
# discard really is like delete, but activites can only be deleted
# before they are submitted for publication, so 'discard' is a more
# accurate description
if self.state != 'preview':
raise exc.HTTPMethodNotAllowed()
activities = VacationActivityCollection(
request.session,
identity=request.identity
)
activities.delete(self)
request.success(_('The activity was discarded'))
@FeriennetApp.view(
model=VacationActivity,
permission=Private,
name='propose',
request_method='POST')
[docs]
def propose_activity(
self: VacationActivity,
request: 'FeriennetRequest'
) -> None:
assert request.app.active_period, 'An active period is required'
# if the latest request has been done in the last minute, this is a
# duplicate and should be ignored
latest = self.latest_request
if latest and (sedate.utcnow() - timedelta(seconds=60)) < latest.created:
return
session = request.session
with session.no_autoflush:
self.propose()
publication_request = self.create_publication_request(
request.app.active_period.materialize(request.session))
ticket = TicketCollection(session).open_ticket(
handler_code='FER',
handler_id=publication_request.id.hex
)
TicketMessage.create(ticket, request, 'opened')
send_ticket_mail(
request=request,
template='mail_ticket_opened.pt',
subject=_('Your ticket has been opened'),
receivers=(self.username, ),
ticket=ticket,
force=(
request.is_organiser_only
or request.current_username != self.username
)
)
if request.email_for_new_tickets:
send_ticket_mail(
request=request,
template='mail_ticket_opened_info.pt',
subject=_('New ticket'),
ticket=ticket,
receivers=(request.email_for_new_tickets, ),
content={
'model': ticket
}
)
request.app.send_websocket(
channel=request.app.websockets_private_channel,
message={
'event': 'browser-notification',
'title': request.translate(_('New ticket')),
'created': ticket.created.isoformat()
}
)
request.success(_('Thank you for your proposal!'))
@request.after
def redirect_intercooler(response: 'Response') -> None:
response.headers.add('X-IC-Redirect', request.link(ticket, 'status'))
# do not redirect here, intercooler doesn't deal well with that...
return
@FeriennetApp.view(
model=VacationActivity,
permission=Secret,
name='accept',
request_method='POST')
[docs]
def accept_activity(
self: VacationActivity,
request: 'FeriennetRequest'
) -> None:
return administer_activity(
model=self,
request=request,
action='accept',
template='mail_activity_accepted.pt',
subject=_('Your activity has been published')
)
@FeriennetApp.view(
model=VacationActivity,
permission=Secret,
name='archive',
request_method='POST')
[docs]
def archive_activity(
self: VacationActivity,
request: 'FeriennetRequest'
) -> None:
return administer_activity(
model=self,
request=request,
action='archive',
template='mail_activity_archived.pt',
subject=_('Your activity has been archived')
)
@FeriennetApp.view(
model=VacationActivity,
permission=Personal,
name='offer-again',
request_method='POST')
[docs]
def offer_activity_again(
self: VacationActivity,
request: 'FeriennetRequest'
) -> None:
assert self.state in ('archived', 'preview')
if self.state == 'archived':
self.state = 'preview'
@request.after
def redirect_intercooler(response: 'Response') -> None:
response.headers.add('X-IC-Redirect', request.link(self, 'edit'))
[docs]
def relevant_ticket(
activity: VacationActivity,
request: 'FeriennetRequest'
) -> 'Ticket | None':
pr = (activity.request_by_period(request.app.active_period)
or activity.latest_request)
if pr:
return TicketCollection(request.session).by_handler_id(pr.id.hex)
return None
[docs]
def administer_activity(
model: VacationActivity,
request: 'FeriennetRequest',
action: str,
template: str,
subject: str
) -> None:
ticket = relevant_ticket(model, request)
if not ticket:
raise RuntimeError(
f'No ticket found for {model.name}, when performing {action}')
# execute state change
getattr(model, action)()
send_ticket_mail(
request=request,
template=template,
subject=subject,
receivers=(model.username, ),
ticket=ticket,
content={
'model': model,
'ticket': ticket
}
)
ActivityMessage.create(ticket, request, action)
@request.after
def redirect_intercooler(response: 'Response') -> None:
response.headers.add('X-IC-Redirect', request.link(ticket))
# do not redirect here, intercooler doesn't deal well with that...
return