flaschengeist-schedule/backend/flaschengeist_events/event_controller.py

438 lines
12 KiB
Python

from datetime import datetime, timedelta, timezone
from enum import IntEnum
from typing import Optional, Tuple
from flaschengeist.models import UtcDateTime
from flaschengeist.models.user import User
from werkzeug.exceptions import BadRequest, Conflict, NotFound
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.util import was_deleted
from flaschengeist import logger
from flaschengeist.database import db
from flaschengeist.plugins import before_delete_user
from flaschengeist.plugins.scheduler import scheduled
from . import EventPlugin
from .models import EventType, Event, Invitation, Job, JobType, Service
# STUB
def _(x):
return x
class NotifyType(IntEnum):
# Invitations 0x00..0x0F
INVITE = 0x01
TRANSFER = 0x02
# Invitation responsed 0x10..0x1F
INVITATION_ACCEPTED = 0x11
INVITATION_REJECTED = 0x12
# Information responses 0x20..0x2F
INFO_ACCEPTED = 0x21
INFO_REJECTED = 0x22
@before_delete_user
def clear_services(user):
"""
This is called when an user got deleted so it cleans future services.
It removes the deleted user from all future events.
"""
logger.debug(f"Clear deleted user {user.userid} from future events.")
_, jobs = get_jobs(user, UtcDateTime.current_utc())
for job in jobs:
job.services = list(filter(lambda s: s.user_ != user, job.services))
db.session.commit()
# def update():
# db.session.commit()
def get_event_types():
return EventType.query.all()
def get_event_type(identifier):
"""Get EventType by ID (int) or name (string)"""
if isinstance(identifier, int):
et = EventType.query.get(identifier)
elif isinstance(identifier, str):
et = EventType.query.filter(EventType.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for EventType")
raise BadRequest
if not et:
raise NotFound
return et
def create_event_type(name):
try:
event = EventType(name=name)
db.session.add(event)
db.session.commit()
return event
except IntegrityError:
raise Conflict("Name already exists")
def rename_event_type(identifier, new_name):
event_type = get_event_type(identifier)
event_type.name = new_name
try:
db.session.commit()
except IntegrityError:
raise Conflict("Name already exists")
def delete_event_type(name):
event_type = get_event_type(name)
db.session.delete(event_type)
try:
db.session.commit()
except IntegrityError:
raise BadRequest("Type still in use")
def get_job_types():
return JobType.query.all()
def get_job_type(type_id):
job_type = JobType.query.get(type_id)
if not job_type:
raise NotFound
return job_type
def create_job_type(name):
try:
job_type = JobType(name=name)
db.session.add(job_type)
db.session.commit()
return job_type
except IntegrityError:
raise BadRequest("Name already exists")
def rename_job_type(name, new_name):
job_type = get_job_type(name)
job_type.name = new_name
try:
db.session.commit()
except IntegrityError:
raise BadRequest("Name already exists")
def delete_job_type(name):
job_type = get_job_type(name)
db.session.delete(job_type)
try:
db.session.commit()
except IntegrityError:
raise BadRequest("Type still in use")
def clear_backup(event: Event):
for job in event.jobs:
services = []
for service in job.services:
if not service.is_backup:
services.append(service)
job.services = services
def get_event(event_id, with_backup=False) -> Event:
event = Event.query.get(event_id)
if event is None:
raise NotFound
if not with_backup:
clear_backup(event)
return event
def get_templates():
return Event.query.filter(Event.is_template == True).all()
def get_events(
start: Optional[datetime] = None,
end: Optional[datetime] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
descending: Optional[bool] = False,
with_backup=False,
) -> Tuple[int, list[Event]]:
"""Query events which start from begin until end
Args:
start (datetime): Earliest start
end (datetime): Latest start
with_backup (bool): Export also backup services
Returns: collection of Event objects
"""
query = Event.query.filter(Event.is_template.__eq__(False))
if start is not None:
query = query.filter(start <= Event.start)
if end is not None:
query = query.filter(Event.start < end)
elif start is None:
# Neither start nor end was given
query = query.filter(datetime.now() <= Event.start)
if descending:
query = query.order_by(Event.start.desc())
else:
query = query.order_by(Event.start)
count = query.count()
if limit is not None:
query = query.limit(limit)
if offset is not None and offset > 0:
query = query.offset(offset)
events: list[Event] = query.all()
if not with_backup:
for event in events:
clear_backup(event)
logger.debug(end)
for event in events:
logger.debug(f"{event.start} < {end} = {event.start < end}")
return count, events
def delete_event(event_id):
"""Delete event with given ID
Args:
event_id: id of Event to delete
Raises:
NotFound if not found
"""
event = get_event(event_id, True)
for job in event.jobs:
delete_job(job)
db.session.delete(event)
db.session.commit()
def create_event(event_type, start, end=None, jobs=[], is_template=None, name=None, description=None):
try:
logger.debug(event_type)
event = Event(
start=start,
end=end,
name=name,
description=description,
type=event_type,
is_template=is_template,
jobs=jobs,
)
db.session.add(event)
db.session.commit()
return event
except IntegrityError:
logger.debug("Database error when creating new event", exc_info=True)
raise BadRequest
def get_job(job_id, event_id=None) -> Job:
query = db.select(Job).where(Job.id == job_id)
if event_id is not None:
query = query.where(Job.event_id_ == event_id)
job = db.session.execute(query).scalar_one_or_none()
if job is None:
raise NotFound
return job
def get_jobs(user, start=None, end=None, limit=None, offset=None, descending=None) -> Tuple[int, list[Job]]:
query = Job.query.join(Service).filter(Service.user_ == user)
if start is not None:
query = query.filter(start <= Job.end)
if end is not None:
query = query.filter(end >= Job.start)
if descending is not None:
query = query.order_by(Job.start.desc(), Job.type)
else:
query = query.order_by(Job.start, Job.type)
count = query.count()
if limit is not None:
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
return count, query.all()
def add_job(event, job_type, required_services, start, end=None, comment=None):
job = Job(
required_services=required_services,
type_=job_type,
start=start,
end=end,
comment=comment,
)
event.jobs.append(job)
update()
return job
def update():
try:
db.session.commit()
except IntegrityError:
logger.debug(
"Error, looks like a Job with that type already exists on an event",
exc_info=True,
)
raise BadRequest()
def delete_job(job: Job):
for service in job.services:
unassign_job(service=service, notify=True)
for invitation in job.invitations_:
respond_invitation(invitation, False)
db.session.delete(job)
db.session.commit()
def assign_job(job: Job, user, value, is_backup=False, notify=False):
assert value > 0
service = Service.query.get((job.id, user.id_))
if service:
service.value = value
service.is_backup = is_backup
else:
job.services.append(Service(user_=user, value=value, is_backup=is_backup, job_=job))
if notify:
EventPlugin.getPlugin().notify(
user,
f"You were assigned to a job\n{job.start.strftime('%d.%m.%Y')}",
{"type": NotifyType.INFO_ACCEPTED, "event_id": job.event_id_},
)
db.session.commit()
def unassign_job(job: Job = None, user=None, service=None, notify=False):
_date = job.start.strftime("%d.%m.%Y")
if service is None:
assert job is not None and user is not None
service = Service.query.get((job.id, user.id_))
else:
user = service.user_
if not service:
raise BadRequest
event_id = service.job_.event_id_
db.session.delete(service)
db.session.commit()
if notify:
EventPlugin.getPlugin().notify(
user, f"Your assignmet was cancelled\n{_date}", {"type": NotifyType.INFO_REJECTED, "event_id": event_id}
)
def invite(job: Job, invitee, inviter, transferee=None):
inv = Invitation(job_=job, inviter_=inviter, invitee_=invitee, transferee_=transferee)
db.session.add(inv)
update()
_date = job.start.strftime("%d.%m.%Y")
if transferee is None:
EventPlugin.getPlugin().notify(
invitee, _(f"Job invitation\n{_date}"), {"type": NotifyType.INVITE, "invitation": inv.id}
)
else:
EventPlugin.getPlugin().notify(
invitee, _(f"Job transfer\n{_date}"), {"type": NotifyType.TRANSFER, "invitation": inv.id}
)
return inv
def get_invitation(id: int):
inv: Invitation = Invitation.query.get(id)
if inv is None:
raise NotFound
return inv
def get_invitations(user: User):
return Invitation.query.filter(
(Invitation.invitee_ == user) | (Invitation.inviter_ == user) | (Invitation.transferee_ == user)
).all()
def cancel_invitation(inv: Invitation):
db.session.delete(inv)
db.session.commit()
def respond_invitation(invite: Invitation, accepted=True):
inviter = invite.inviter_
job = invite.job_
db.session.delete(invite)
db.session.commit()
if not was_deleted(invite):
raise Conflict
if not accepted:
EventPlugin.getPlugin().notify(
inviter,
_("Invitation rejected"),
{
"type": NotifyType.INVITATION_REJECTED,
"event": job.event_id_,
"job": invite.job_id,
"invitee": invite.invitee_id,
},
)
else:
if invite.transferee_id is None:
assign_job(job, invite.invitee_, 1)
else:
service = filter(lambda s: s.userid == invite.transferee_id, job.services)
if not service:
raise Conflict
unassign_job(job, invite.transferee_, service[0], True)
assign_job(job, invite.invitee_, service[0].value)
EventPlugin.getPlugin().notify(
inviter,
_("Invitation accepted"),
{
"type": NotifyType.INVITATION_ACCEPTED,
"event": job.event_id_,
"job": invite.job_id,
"invitee": invite.invitee_id,
},
)
@scheduled(id="dev.flaschengeist.events.assign_backups", minutes=30)
def assign_backups():
now = datetime.now(tz=timezone.utc)
# now + backup_time + next cron tick
start = now + timedelta(hours=16) + timedelta(minutes=30)
services = Service.query.filter(Service.is_backup == True).join(Job).filter(Job.start <= start).all()
for service in services:
if service.job_.start <= now or service.job_.is_full():
EventPlugin.getPlugin().notify(
service.user_,
"Your backup assignment was cancelled.",
{"event_id": service.job_.event_id_},
)
logger.debug(f"Service is outdated or full, removing. {service.serialize()}")
db.session.delete(service)
else:
service.is_backup = False
logger.debug(f"Service not full, assigning backup. {service.serialize()}")
EventPlugin.getPlugin().notify(
service.user_,
"Your backup assignment was accepted.",
{"event_id": service.job_.event_id_},
)
db.session.commit()