2021-11-28 21:30:15 +00:00
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from enum import IntEnum
|
|
|
|
from typing import Optional, Tuple
|
2021-12-02 20:30:42 +00:00
|
|
|
from flaschengeist.models import UtcDateTime
|
2021-12-22 00:06:23 +00:00
|
|
|
from flaschengeist.models.user import User
|
2021-11-28 21:30:15 +00:00
|
|
|
|
|
|
|
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
|
2021-12-02 20:30:42 +00:00
|
|
|
from flaschengeist.plugins import before_delete_user
|
2021-12-06 22:49:27 +00:00
|
|
|
from flaschengeist.plugins.scheduler import scheduled
|
2021-11-28 21:30:15 +00:00
|
|
|
|
|
|
|
from . import EventPlugin
|
|
|
|
from .models import EventType, Event, Invitation, Job, JobType, Service
|
|
|
|
|
2023-04-26 09:28:16 +00:00
|
|
|
|
2021-11-28 21:30:15 +00:00
|
|
|
# STUB
|
|
|
|
def _(x):
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
|
|
class NotifyType(IntEnum):
|
|
|
|
# Invitations 0x00..0x0F
|
|
|
|
INVITE = 0x01
|
|
|
|
TRANSFER = 0x02
|
|
|
|
# Invitation responsed 0x10..0x1F
|
|
|
|
INVITATION_ACCEPTED = 0x10
|
|
|
|
INVITATION_REJECTED = 0x11
|
|
|
|
|
|
|
|
|
2021-12-02 20:30:42 +00:00
|
|
|
@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()
|
|
|
|
|
|
|
|
|
2023-04-26 09:28:16 +00:00
|
|
|
# def update():
|
|
|
|
# db.session.commit()
|
2021-11-28 21:30:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
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:
|
2023-04-26 09:28:16 +00:00
|
|
|
query = db.select(Job).where(Job.id == job_id)
|
2021-11-28 21:30:15 +00:00
|
|
|
if event_id is not None:
|
2023-04-26 09:28:16 +00:00
|
|
|
query = query.where(Job.event_id_ == event_id)
|
|
|
|
job = db.session.execute(query).scalar_one_or_none()
|
2021-11-28 21:30:15 +00:00
|
|
|
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:
|
2021-12-22 00:05:10 +00:00
|
|
|
query = query.order_by(Job.start.desc(), Job.type)
|
2021-11-28 21:30:15 +00:00
|
|
|
else:
|
2021-12-22 00:05:10 +00:00
|
|
|
query = query.order_by(Job.start, Job.type)
|
2021-11-28 21:30:15 +00:00
|
|
|
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,
|
2021-12-22 00:05:10 +00:00
|
|
|
type_=job_type,
|
2021-11-28 21:30:15 +00:00
|
|
|
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):
|
|
|
|
assert value > 0
|
|
|
|
service = Service.query.get((job.id, user.id_))
|
|
|
|
if service:
|
|
|
|
service.value = value
|
2021-12-02 20:31:20 +00:00
|
|
|
service.is_backup = is_backup
|
2021-11-28 21:30:15 +00:00
|
|
|
else:
|
|
|
|
job.services.append(Service(user_=user, value=value, is_backup=is_backup, job_=job))
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
def unassign_job(job: Job = None, user=None, service=None, notify=False):
|
|
|
|
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.plugin.notify(user, "Your assignmet was cancelled", {"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()
|
|
|
|
if transferee is None:
|
|
|
|
EventPlugin.plugin.notify(invitee, _("Job invitation"), {"type": NotifyType.INVITE, "invitation": inv.id})
|
|
|
|
else:
|
|
|
|
EventPlugin.plugin.notify(invitee, _("Job transfer"), {"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
|
|
|
|
|
|
|
|
|
2021-12-22 00:06:23 +00:00
|
|
|
def get_invitations(user: User):
|
|
|
|
return Invitation.query.filter(
|
|
|
|
(Invitation.invitee_ == user) | (Invitation.inviter_ == user) | (Invitation.transferee_ == user)
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
2021-11-28 21:30:15 +00:00
|
|
|
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.plugin.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.plugin.notify(
|
|
|
|
inviter,
|
|
|
|
_("Invitation accepted"),
|
|
|
|
{
|
|
|
|
"type": NotifyType.INVITATION_ACCEPTED,
|
|
|
|
"event": job.event_id_,
|
|
|
|
"job": invite.job_id,
|
|
|
|
"invitee": invite.invitee_id,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-12-22 00:05:10 +00:00
|
|
|
@scheduled(id="dev.flaschengeist.events.assign_backups", minutes=30)
|
2021-11-28 21:30:15 +00:00
|
|
|
def assign_backups():
|
|
|
|
now = datetime.now(tz=timezone.utc)
|
|
|
|
# now + backup_time + next cron tick
|
|
|
|
start = now + timedelta(hours=16) + timedelta(minutes=30)
|
2021-12-06 22:49:27 +00:00
|
|
|
|
2021-11-28 21:30:15 +00:00
|
|
|
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.plugin.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.plugin.notify(
|
|
|
|
service.user_,
|
|
|
|
"Your backup assignment was accepted.",
|
|
|
|
{"event_id": service.job_.event_id_},
|
|
|
|
)
|
|
|
|
db.session.commit()
|