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
|
||||
# No need, this is done by user
|
||||
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