chore(backend) Split backend from flaschengeist, now developed here
This commit is contained in:
parent
c8ae458775
commit
de6e959937
|
@ -3,3 +3,7 @@ node_modules/
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
# No need, this is done by user
|
# No need, this is done by user
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
*.egg-info
|
||||||
|
__pycache__
|
|
@ -0,0 +1,2 @@
|
||||||
|
yarn-error.log
|
||||||
|
backend/
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Events plugin
|
||||||
|
|
||||||
|
Provides duty schedule / duty roster functions
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, current_app
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from flaschengeist.plugins import Plugin
|
||||||
|
from . import permissions, models
|
||||||
|
|
||||||
|
|
||||||
|
class EventPlugin(Plugin):
|
||||||
|
name = "events"
|
||||||
|
id = "dev.flaschengeist.plugins.events"
|
||||||
|
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name])
|
||||||
|
permissions = permissions.permissions
|
||||||
|
blueprint = Blueprint(name, __name__)
|
||||||
|
models = models
|
||||||
|
|
||||||
|
def __init__(self, cfg):
|
||||||
|
super(EventPlugin, self).__init__(cfg)
|
||||||
|
from . import routes
|
|
@ -0,0 +1,397 @@
|
||||||
|
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()
|
|
@ -0,0 +1,140 @@
|
||||||
|
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
|
from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
|
from flaschengeist.models.user import User
|
||||||
|
from flaschengeist.database import db
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Types #
|
||||||
|
#########
|
||||||
|
|
||||||
|
_table_prefix_ = "events_"
|
||||||
|
|
||||||
|
|
||||||
|
class EventType(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "event_type"
|
||||||
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
|
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class JobType(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "job_type"
|
||||||
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
|
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
########
|
||||||
|
# Jobs #
|
||||||
|
########
|
||||||
|
|
||||||
|
|
||||||
|
class Service(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "service"
|
||||||
|
userid: str = ""
|
||||||
|
is_backup: bool = db.Column(db.Boolean, default=False)
|
||||||
|
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
|
_job_id = db.Column(
|
||||||
|
"job_id",
|
||||||
|
Serial,
|
||||||
|
db.ForeignKey(f"{_table_prefix_}job.id"),
|
||||||
|
nullable=False,
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
_user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False, primary_key=True)
|
||||||
|
|
||||||
|
user_: User = db.relationship("User")
|
||||||
|
job_: Job = db.relationship("Job")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def userid(self):
|
||||||
|
return self.user_.userid
|
||||||
|
|
||||||
|
|
||||||
|
class Job(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "job"
|
||||||
|
|
||||||
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
|
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||||
|
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
|
type: Union[JobType, int] = db.relationship("JobType")
|
||||||
|
comment: Optional[str] = db.Column(db.String(256))
|
||||||
|
locked: bool = db.Column(db.Boolean(), default=False, nullable=False)
|
||||||
|
services: list[Service] = db.relationship(
|
||||||
|
"Service", back_populates="job_", cascade="save-update, merge, delete, delete-orphan"
|
||||||
|
)
|
||||||
|
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
|
event_ = db.relationship("Event", back_populates="jobs")
|
||||||
|
event_id_ = db.Column("event_id", Serial, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
|
||||||
|
type_id_ = db.Column("type_id", Serial, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
|
||||||
|
|
||||||
|
invitations_ = db.relationship("Invitation", cascade="all,delete,delete-orphan", back_populates="job_")
|
||||||
|
|
||||||
|
__table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),)
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Events #
|
||||||
|
##########
|
||||||
|
class Event(db.Model, ModelSerializeMixin):
|
||||||
|
"""Model for an Event"""
|
||||||
|
|
||||||
|
__tablename__ = _table_prefix_ + "event"
|
||||||
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
|
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||||
|
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
|
name: Optional[str] = db.Column(db.String(255))
|
||||||
|
description: Optional[str] = db.Column(db.String(512))
|
||||||
|
type: Union[EventType, int] = db.relationship("EventType")
|
||||||
|
is_template: bool = db.Column(db.Boolean, default=False)
|
||||||
|
jobs: list[Job] = db.relationship(
|
||||||
|
"Job",
|
||||||
|
back_populates="event_",
|
||||||
|
cascade="all,delete,delete-orphan",
|
||||||
|
order_by="[Job.start, Job.end]",
|
||||||
|
)
|
||||||
|
# Protected for internal use
|
||||||
|
_type_id = db.Column(
|
||||||
|
"type_id",
|
||||||
|
Serial,
|
||||||
|
db.ForeignKey(f"{_table_prefix_}event_type.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Invitation(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "invitation"
|
||||||
|
|
||||||
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
|
job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
|
||||||
|
# Dummy properties for API export
|
||||||
|
invitee_id: str = None # User who was invited to take over
|
||||||
|
inviter_id: str = None # User who invited the invitee
|
||||||
|
transferee_id: Optional[str] = None # In case of a transfer: The user who is transfered out of the job
|
||||||
|
# Not exported properties for backend use
|
||||||
|
job_: Job = db.relationship(Job, foreign_keys="Invitation.job_id")
|
||||||
|
invitee_: User = db.relationship("User", foreign_keys="Invitation._invitee_id")
|
||||||
|
inviter_: User = db.relationship("User", foreign_keys="Invitation._inviter_id")
|
||||||
|
transferee_: User = db.relationship("User", foreign_keys="Invitation._transferee_id")
|
||||||
|
# Protected properties needed for internal use
|
||||||
|
_invitee_id = db.Column("invitee_id", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
_inviter_id = db.Column("inviter_id", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
_transferee_id = db.Column("transferee_id", Serial, db.ForeignKey("user.id"))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def invitee_id(self):
|
||||||
|
return self.invitee_.userid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inviter_id(self):
|
||||||
|
return self.inviter_.userid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def transferee_id(self):
|
||||||
|
return self.transferee_.userid if self.transferee_ else None
|
|
@ -0,0 +1,28 @@
|
||||||
|
CREATE = "events_create"
|
||||||
|
"""Can create events"""
|
||||||
|
|
||||||
|
EDIT = "events_edit"
|
||||||
|
"""Can edit events"""
|
||||||
|
|
||||||
|
DELETE = "events_delete"
|
||||||
|
"""Can delete events"""
|
||||||
|
|
||||||
|
EVENT_TYPE = "events_event_type"
|
||||||
|
"""Can create and edit EventTypes"""
|
||||||
|
|
||||||
|
JOB_TYPE = "events_job_type"
|
||||||
|
"""Can create and edit JobTypes"""
|
||||||
|
|
||||||
|
ASSIGN = "events_assign"
|
||||||
|
"""Can self assign to jobs"""
|
||||||
|
|
||||||
|
ASSIGN_OTHER = "events_assign_other"
|
||||||
|
"""Can assign other users to jobs"""
|
||||||
|
|
||||||
|
SEE_BACKUP = "events_see_backup"
|
||||||
|
"""Can see users assigned as backup"""
|
||||||
|
|
||||||
|
LOCK_JOBS = "events_lock_jobs"
|
||||||
|
"""Can lock jobs, no further services can be assigned or unassigned"""
|
||||||
|
|
||||||
|
permissions = [value for key, value in globals().items() if not key.startswith("_")]
|
|
@ -0,0 +1,528 @@
|
||||||
|
from http.client import NO_CONTENT
|
||||||
|
from flask import request, jsonify
|
||||||
|
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
|
||||||
|
|
||||||
|
from flaschengeist.models.session import Session
|
||||||
|
from flaschengeist.controller import userController
|
||||||
|
from flaschengeist.utils.decorators import login_required
|
||||||
|
from flaschengeist.utils.datetime import from_iso_format
|
||||||
|
from flaschengeist.utils.HTTP import get_filter_args, no_content
|
||||||
|
|
||||||
|
from . import event_controller, permissions, EventPlugin
|
||||||
|
|
||||||
|
|
||||||
|
def dict_get(self, key, default=None, type=None):
|
||||||
|
"""Same as .get from MultiDict"""
|
||||||
|
try:
|
||||||
|
rv = self[key]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
if type is not None:
|
||||||
|
try:
|
||||||
|
rv = type(rv)
|
||||||
|
except ValueError:
|
||||||
|
rv = default
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/templates", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_templates(current_session):
|
||||||
|
return jsonify(event_controller.get_templates())
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/event-types", methods=["GET"])
|
||||||
|
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_event_types(current_session, identifier=None):
|
||||||
|
"""Get EventType(s)
|
||||||
|
|
||||||
|
Route: ``/events/event-types`` | Method: ``GET``
|
||||||
|
Route: ``/events/event-types/<identifier>`` | Method: ``GET``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
identifier: If querying a specific EventType
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded (list of) EventType(s) or HTTP-error
|
||||||
|
"""
|
||||||
|
if identifier:
|
||||||
|
result = event_controller.get_event_type(identifier)
|
||||||
|
else:
|
||||||
|
result = event_controller.get_event_types()
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/event-types", methods=["POST"])
|
||||||
|
@login_required(permission=permissions.EVENT_TYPE)
|
||||||
|
def new_event_type(current_session):
|
||||||
|
"""Create a new EventType
|
||||||
|
|
||||||
|
Route: ``/events/event-types`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: ``{name: string}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-Created or HTTP-error
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
if "name" not in data:
|
||||||
|
raise BadRequest
|
||||||
|
event_type = event_controller.create_event_type(data["name"])
|
||||||
|
return jsonify(event_type)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
|
||||||
|
@login_required(permission=permissions.EVENT_TYPE)
|
||||||
|
def modify_event_type(identifier, current_session):
|
||||||
|
"""Rename or delete an event type
|
||||||
|
|
||||||
|
Route: ``/events/event-types/<id>`` | Method: ``PUT`` or ``DELETE``
|
||||||
|
|
||||||
|
POST-data: (if renaming) ``{name: string}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identifier: Identifier of the EventType
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-NoContent or HTTP-error
|
||||||
|
"""
|
||||||
|
if request.method == "DELETE":
|
||||||
|
event_controller.delete_event_type(identifier)
|
||||||
|
else:
|
||||||
|
data = request.get_json()
|
||||||
|
if "name" not in data:
|
||||||
|
raise BadRequest("Parameter missing in data")
|
||||||
|
event_controller.rename_event_type(identifier, data["name"])
|
||||||
|
return "", NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/job-types", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_job_types(current_session):
|
||||||
|
"""Get all JobTypes
|
||||||
|
|
||||||
|
Route: ``/events/job-types`` | Method: ``GET``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded list of JobType HTTP-error
|
||||||
|
"""
|
||||||
|
types = event_controller.get_job_types()
|
||||||
|
return jsonify(types)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/job-types", methods=["POST"])
|
||||||
|
@login_required(permission=permissions.JOB_TYPE)
|
||||||
|
def new_job_type(current_session):
|
||||||
|
"""Create a new JobType
|
||||||
|
|
||||||
|
Route: ``/events/job-types`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: ``{name: string}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded JobType or HTTP-error
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
if "name" not in data:
|
||||||
|
raise BadRequest
|
||||||
|
jt = event_controller.create_job_type(data["name"])
|
||||||
|
return jsonify(jt)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
|
||||||
|
@login_required(permission=permissions.JOB_TYPE)
|
||||||
|
def modify_job_type(type_id, current_session):
|
||||||
|
"""Rename or delete a JobType
|
||||||
|
|
||||||
|
Route: ``/events/job-types/<name>`` | Method: ``PUT`` or ``DELETE``
|
||||||
|
|
||||||
|
POST-data: (if renaming) ``{name: string}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_id: Identifier of the JobType
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-NoContent or HTTP-error
|
||||||
|
"""
|
||||||
|
if request.method == "DELETE":
|
||||||
|
event_controller.delete_job_type(type_id)
|
||||||
|
else:
|
||||||
|
data = request.get_json()
|
||||||
|
if "name" not in data:
|
||||||
|
raise BadRequest("Parameter missing in data")
|
||||||
|
event_controller.rename_job_type(type_id, data["name"])
|
||||||
|
return "", NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_event(event_id, current_session):
|
||||||
|
"""Get event by id
|
||||||
|
|
||||||
|
Route: ``/events/<event_id>`` | Method: ``GET``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_id: ID identifying the event
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded event object
|
||||||
|
"""
|
||||||
|
event = event_controller.get_event(
|
||||||
|
event_id,
|
||||||
|
with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP),
|
||||||
|
)
|
||||||
|
return jsonify(event)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_events(current_session):
|
||||||
|
count, result = event_controller.get_events(
|
||||||
|
*get_filter_args(),
|
||||||
|
with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP),
|
||||||
|
)
|
||||||
|
return jsonify({"count": count, "result": result})
|
||||||
|
|
||||||
|
|
||||||
|
def _add_job(event, data):
|
||||||
|
try:
|
||||||
|
start = from_iso_format(data["start"])
|
||||||
|
end = dict_get(data, "end", None, type=from_iso_format)
|
||||||
|
required_services = data["required_services"]
|
||||||
|
job_type = data["type"]
|
||||||
|
if isinstance(job_type, dict):
|
||||||
|
job_type = data["type"]["id"]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
raise BadRequest("Missing or invalid POST parameter")
|
||||||
|
|
||||||
|
job_type = event_controller.get_job_type(job_type)
|
||||||
|
event_controller.add_job(
|
||||||
|
event,
|
||||||
|
job_type,
|
||||||
|
required_services,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
comment=dict_get(data, "comment", None, str),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events", methods=["POST"])
|
||||||
|
@login_required(permission=permissions.CREATE)
|
||||||
|
def create_event(current_session):
|
||||||
|
"""Create an new event
|
||||||
|
|
||||||
|
Route: ``/events`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: See interfaces for Event, can already contain jobs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded Event object or HTTP-error
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
try:
|
||||||
|
start = from_iso_format(data["start"])
|
||||||
|
end = dict_get(data, "end", None, type=from_iso_format)
|
||||||
|
data_type = data["type"]
|
||||||
|
if isinstance(data_type, dict):
|
||||||
|
data_type = data["type"]["id"]
|
||||||
|
event_type = event_controller.get_event_type(data_type)
|
||||||
|
except KeyError:
|
||||||
|
raise BadRequest("Missing POST parameter")
|
||||||
|
except (NotFound, ValueError):
|
||||||
|
raise BadRequest("Invalid parameter")
|
||||||
|
|
||||||
|
event = event_controller.create_event(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
name=dict_get(data, "name", None),
|
||||||
|
is_template=dict_get(data, "is_template", None),
|
||||||
|
event_type=event_type,
|
||||||
|
description=dict_get(data, "description", None),
|
||||||
|
)
|
||||||
|
if "jobs" in data:
|
||||||
|
for job in data["jobs"]:
|
||||||
|
_add_job(event, job)
|
||||||
|
|
||||||
|
return jsonify(event)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["PUT"])
|
||||||
|
@login_required(permission=permissions.EDIT)
|
||||||
|
def modify_event(event_id, current_session):
|
||||||
|
"""Modify an event
|
||||||
|
|
||||||
|
Route: ``/events/<event_id>`` | Method: ``PUT``
|
||||||
|
|
||||||
|
POST-data: See interfaces for Event, can already contain slots
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_id: Identifier of the event
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded Event object or HTTP-error
|
||||||
|
"""
|
||||||
|
event = event_controller.get_event(event_id)
|
||||||
|
data = request.get_json()
|
||||||
|
event.start = dict_get(data, "start", event.start, type=from_iso_format)
|
||||||
|
event.end = dict_get(data, "end", event.end, type=from_iso_format)
|
||||||
|
event.name = dict_get(data, "name", event.name, type=str)
|
||||||
|
event.description = dict_get(data, "description", event.description, type=str)
|
||||||
|
if "type" in data:
|
||||||
|
event_type = event_controller.get_event_type(data["type"])
|
||||||
|
event.type = event_type
|
||||||
|
|
||||||
|
event_controller.update()
|
||||||
|
return jsonify(event)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["DELETE"])
|
||||||
|
@login_required(permission=permissions.DELETE)
|
||||||
|
def delete_event(event_id, current_session):
|
||||||
|
"""Delete an event
|
||||||
|
|
||||||
|
Route: ``/events/<event_id>`` | Method: ``DELETE``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_id: Identifier of the event
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-NoContent or HTTP-error
|
||||||
|
"""
|
||||||
|
event_controller.delete_event(event_id)
|
||||||
|
return "", NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs", methods=["POST"])
|
||||||
|
@login_required(permission=permissions.EDIT)
|
||||||
|
def add_job(event_id, current_session):
|
||||||
|
"""Add an new Job to an Event / EventSlot
|
||||||
|
|
||||||
|
Route: ``/events/<event_id>/jobs`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: See Job
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_id: Identifier of the event
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded Event object or HTTP-error
|
||||||
|
"""
|
||||||
|
event = event_controller.get_event(event_id)
|
||||||
|
_add_job(event, request.get_json())
|
||||||
|
return jsonify(event)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
|
||||||
|
@login_required(permission=permissions.DELETE)
|
||||||
|
def delete_job(event_id, job_id, current_session):
|
||||||
|
"""Delete a Job
|
||||||
|
|
||||||
|
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``DELETE``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_id: Identifier of the event
|
||||||
|
job_id: Identifier of the Job
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-no-content or HTTP error
|
||||||
|
"""
|
||||||
|
job = event_controller.get_job(job_id, event_id)
|
||||||
|
event_controller.delete_job(job)
|
||||||
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
|
||||||
|
@login_required()
|
||||||
|
def update_job(event_id, job_id, current_session: Session):
|
||||||
|
"""Edit Job
|
||||||
|
|
||||||
|
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``PUT``
|
||||||
|
|
||||||
|
POST-data: See TS interface for Job
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_id: Identifier of the event
|
||||||
|
job_id: Identifier of the Job
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded Job object or HTTP-error
|
||||||
|
"""
|
||||||
|
if not current_session.user_.has_permission(permissions.EDIT):
|
||||||
|
raise Forbidden
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
|
job = event_controller.get_job(job_id, event_id)
|
||||||
|
try:
|
||||||
|
if "type" in data:
|
||||||
|
job.type = event_controller.get_job_type(data["type"])
|
||||||
|
job.start = from_iso_format(data.get("start", job.start))
|
||||||
|
job.end = from_iso_format(data.get("end", job.end))
|
||||||
|
job.comment = str(data.get("comment", job.comment))
|
||||||
|
job.locked = bool(data.get("locked", job.locked))
|
||||||
|
job.required_services = float(data.get("required_services", job.required_services))
|
||||||
|
event_controller.update()
|
||||||
|
except NotFound:
|
||||||
|
raise BadRequest("Invalid JobType")
|
||||||
|
except ValueError:
|
||||||
|
raise BadRequest("Invalid POST data")
|
||||||
|
|
||||||
|
return jsonify(job)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/jobs", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_jobs(current_session: Session):
|
||||||
|
count, result = event_controller.get_jobs(current_session.user_, *get_filter_args())
|
||||||
|
return jsonify({"count": count, "result": result})
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/assign", methods=["POST"])
|
||||||
|
@login_required()
|
||||||
|
def assign_job(job_id, current_session: Session):
|
||||||
|
"""Assign / unassign user to the Job
|
||||||
|
|
||||||
|
Route: ``/events/jobs/<job_id>/assign`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: a Service object, see TS interface for Service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: Identifier of the Job
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON encoded Job or HTTP-error
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
job = event_controller.get_job(job_id)
|
||||||
|
try:
|
||||||
|
user = userController.get_user(data["userid"])
|
||||||
|
value = data["value"]
|
||||||
|
if (user == current_session.user_ and not user.has_permission(permissions.ASSIGN)) or (
|
||||||
|
user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
|
||||||
|
):
|
||||||
|
raise Forbidden
|
||||||
|
if value > 0:
|
||||||
|
event_controller.assign_job(job, user, value, data.get("is_backup", False))
|
||||||
|
else:
|
||||||
|
event_controller.unassign_job(job, user, notify=user != current_session.user_)
|
||||||
|
except (TypeError, KeyError, ValueError):
|
||||||
|
raise BadRequest
|
||||||
|
return jsonify(job)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/lock", methods=["POST"])
|
||||||
|
@login_required(permissions.LOCK_JOBS)
|
||||||
|
def lock_job(job_id, current_session: Session):
|
||||||
|
"""Lock / unlock the Job
|
||||||
|
|
||||||
|
Route: ``/events/jobs/<job_id>/lock`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: ``{locked: boolean}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: Identifier of the Job
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-No-Content or HTTP-error
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
job = event_controller.get_job(job_id)
|
||||||
|
try:
|
||||||
|
locked = bool(userController.get_user(data["locked"]))
|
||||||
|
job.locked = locked
|
||||||
|
event_controller.update()
|
||||||
|
except (TypeError, KeyError, ValueError):
|
||||||
|
raise BadRequest
|
||||||
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/invitations", methods=["POST"])
|
||||||
|
@login_required()
|
||||||
|
def invite(current_session: Session):
|
||||||
|
"""Invite an user to a job or transfer job
|
||||||
|
|
||||||
|
Route: ``/events/invites`` | Method: ``POST``
|
||||||
|
|
||||||
|
POST-data: ``{job: number, invitees: string[], is_transfer?: boolean}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Invitation objects or HTTP-error
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
transferee = data.get("transferee", None)
|
||||||
|
if (
|
||||||
|
transferee is not None
|
||||||
|
and transferee != current_session.userid
|
||||||
|
and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
|
||||||
|
):
|
||||||
|
raise Forbidden
|
||||||
|
|
||||||
|
try:
|
||||||
|
job = event_controller.get_job(data["job"])
|
||||||
|
if not isinstance(data["invitees"], list):
|
||||||
|
raise BadRequest
|
||||||
|
return jsonify(
|
||||||
|
[
|
||||||
|
event_controller.invite(job, invitee, current_session.user_, transferee)
|
||||||
|
for invitee in [userController.get_user(uid) for uid in data["invitees"]]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
except (TypeError, KeyError, ValueError):
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_invitation(invitation_id: int, current_session: Session):
|
||||||
|
inv = event_controller.get_invitation(invitation_id)
|
||||||
|
if current_session.userid not in [inv.invitee_id, inv.inviter_id, inv.transferee_id]:
|
||||||
|
raise Forbidden
|
||||||
|
return jsonify(inv)
|
||||||
|
|
||||||
|
|
||||||
|
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["DELETE", "PUT"])
|
||||||
|
@login_required()
|
||||||
|
def respond_invitation(invitation_id: int, current_session: Session):
|
||||||
|
inv = event_controller.get_invitation(invitation_id)
|
||||||
|
if request.method == "DELETE":
|
||||||
|
if current_session.userid == inv.invitee_id:
|
||||||
|
event_controller.respond_invitation(inv, False)
|
||||||
|
elif current_session.userid == inv.inviter_id:
|
||||||
|
event_controller.cancel_invitation(inv)
|
||||||
|
else:
|
||||||
|
raise Forbidden
|
||||||
|
else:
|
||||||
|
# maybe validate data is something like ({accepted: true})
|
||||||
|
if current_session.userid != inv.invitee_id:
|
||||||
|
raise Forbidden
|
||||||
|
event_controller.respond_invitation(inv)
|
||||||
|
return no_content()
|
|
@ -0,0 +1,3 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
|
@ -0,0 +1,30 @@
|
||||||
|
[metadata]
|
||||||
|
license = MIT
|
||||||
|
version = 0.0.1-dev.1
|
||||||
|
name = flaschengeist-events
|
||||||
|
description = Events plugin for Flaschengeist
|
||||||
|
url = https://flaschengeist.dev/Flaschengeist/flaschengeist-schedule
|
||||||
|
project_urls =
|
||||||
|
Source = https://flaschengeist.dev/Flaschengeist/flaschengeist-schedule
|
||||||
|
Tracker = https://flaschengeist.dev/Flaschengeist/flaschengeist-schedule/issues
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.8,
|
||||||
|
Programming Language :: Python :: 3.9,
|
||||||
|
Programming Language :: Python :: 3.10,
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
|
||||||
|
|
||||||
|
[bdist_wheel]
|
||||||
|
universal = True
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages =
|
||||||
|
flaschengeist_events
|
||||||
|
install_requires =
|
||||||
|
flaschengeist == 2.0.*
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
flaschengeist.plugins =
|
||||||
|
events = flaschengeist_events:EventPlugin
|
Loading…
Reference in New Issue