from datetime import datetime, timedelta, timezone from enum import IntEnum from typing import Optional, Tuple 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.utils.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 = 0x10 INVITATION_REJECTED = 0x11 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) print(job_type) 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 = Job.query.filter(Job.id == job_id) if event_id is not None: query = query.filter(Job.event_id_ == event_id) job = query.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_id_) else: query = query.order_by(Job.start, Job.type_id_) 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): assert value > 0 service = Service.query.get((job.id, user.id_)) if service: service.value = value 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 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, }, ) @scheduled def assign_backups(): logger.debug("Notifications") 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.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()