[Plugin] schedule: Working Event, EventSlot, EventType and JobType
This commit is contained in:
parent
363ec6530b
commit
4da4c1ee01
|
@ -2,24 +2,25 @@
|
|||
|
||||
Provides duty schedule / duty roster functions
|
||||
"""
|
||||
import http
|
||||
|
||||
from dateutil import parser
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.client import NO_CONTENT, CREATED
|
||||
from flask import Blueprint, request, jsonify
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
from backports.datetime_fromisoformat import MonkeyPatch
|
||||
|
||||
from flaschengeist.database import db
|
||||
from flaschengeist.plugins import Plugin
|
||||
from flaschengeist.decorator import login_required
|
||||
|
||||
from . import event_controller, permissions
|
||||
from .models import EventType
|
||||
from . import models
|
||||
|
||||
MonkeyPatch.patch_fromisoformat()
|
||||
schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule")
|
||||
|
||||
|
||||
class SchedulePlugin(Plugin):
|
||||
models = models
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(
|
||||
blueprint=schedule_bp,
|
||||
|
@ -63,7 +64,7 @@ def new_event_type(current_session):
|
|||
if "name" not in data:
|
||||
raise BadRequest
|
||||
event_controller.create_event_type(data["name"])
|
||||
return "", http.HTTPStatus.CREATED
|
||||
return "", CREATED
|
||||
|
||||
|
||||
@schedule_bp.route("/event-types/<name>", methods=["PUT", "DELETE"])
|
||||
|
@ -89,7 +90,7 @@ def modify_event_type(name, current_session):
|
|||
if "name" not in data:
|
||||
raise BadRequest("Parameter missing in data")
|
||||
event_controller.rename_event_type(name, data["name"])
|
||||
return "", http.HTTPStatus.NO_CONTENT
|
||||
return "", NO_CONTENT
|
||||
|
||||
|
||||
@schedule_bp.route("/job-types", methods=["GET"])
|
||||
|
@ -128,7 +129,7 @@ def new_job_type(current_session):
|
|||
if "name" not in data:
|
||||
raise BadRequest
|
||||
event_controller.create_job_type(data["name"])
|
||||
return "", http.HTTPStatus.CREATED
|
||||
return "", CREATED
|
||||
|
||||
|
||||
@schedule_bp.route("/job-types/<name>", methods=["PUT", "DELETE"])
|
||||
|
@ -154,10 +155,9 @@ def modify_job_type(name, current_session):
|
|||
if "name" not in data:
|
||||
raise BadRequest("Parameter missing in data")
|
||||
event_controller.rename_name_type(name, data["name"])
|
||||
return "", http.HTTPStatus.NO_CONTENT
|
||||
return "", NO_CONTENT
|
||||
|
||||
|
||||
########### TODO: Ab hier ############
|
||||
@schedule_bp.route("/events/<int:event_id>", methods=["GET"])
|
||||
@login_required()
|
||||
def get_event(event_id, current_session):
|
||||
|
@ -173,8 +173,6 @@ def get_event(event_id, current_session):
|
|||
JSON encoded event object
|
||||
"""
|
||||
event = event_controller.get_event(event_id)
|
||||
if not event:
|
||||
raise NotFound
|
||||
return jsonify(event)
|
||||
|
||||
|
||||
|
@ -196,9 +194,6 @@ def get_events(current_session, year=datetime.now().year, month=datetime.now().m
|
|||
|
||||
Returns:
|
||||
JSON encoded list containing events found or HTTP-error
|
||||
|
||||
Raises:
|
||||
BadRequest: If date is invalid
|
||||
"""
|
||||
try:
|
||||
begin = datetime(year=year, month=month, day=1)
|
||||
|
@ -217,88 +212,169 @@ def get_events(current_session, year=datetime.now().year, month=datetime.now().m
|
|||
raise BadRequest("Invalid date given")
|
||||
|
||||
|
||||
def _event_slot_from_data(data):
|
||||
try:
|
||||
start = datetime.fromisoformat(data["start"]).replace(tzinfo=timezone.utc)
|
||||
end = datetime.fromisoformat(data["end"]).replace(tzinfo=timezone.utc) if "end" in data else None
|
||||
# jobs = ...
|
||||
except (NotFound, KeyError, ValueError):
|
||||
raise BadRequest("Missing POST parameter")
|
||||
return {"start": start, "end": end}
|
||||
|
||||
|
||||
@schedule_bp.route("/events", methods=["POST"])
|
||||
@login_required(permission=permissions.CREATE)
|
||||
def __new_event(**kwargs):
|
||||
def create_event(current_session):
|
||||
"""Create an new event
|
||||
|
||||
Route: ``/events`` | Method: ``POST``
|
||||
|
||||
POST-data: See interfaces for Event, can already contain slots
|
||||
|
||||
Args:
|
||||
current_session: Session sent with Authorization Header
|
||||
|
||||
Returns:
|
||||
JSON encoded Event object or HTTP-error
|
||||
"""
|
||||
data = request.get_json()
|
||||
try:
|
||||
start = datetime.fromisoformat(data["start"]).replace(tzinfo=timezone.utc)
|
||||
event_type = event_controller.get_event_type(data["type"])
|
||||
except (NotFound, KeyError, ValueError):
|
||||
raise BadRequest("Missing POST parameter")
|
||||
|
||||
data = request.get_json()
|
||||
event = event_controller.create_event(
|
||||
begin=parser.isoparse(data["begin"]),
|
||||
end=parser.isoparse(data["end"]),
|
||||
description=data["description"],
|
||||
type=EventType.query.get(data["kind"]),
|
||||
start=start, event_type=event_type, description=data["description"] if "description" in data else None
|
||||
)
|
||||
return jsonify({"ok": "ok", "id": event.id})
|
||||
if "slots" in data:
|
||||
for slot in data["slots"]:
|
||||
event_controller.add_event_slot(event, **_event_slot_from_data(slot))
|
||||
return jsonify(event)
|
||||
|
||||
|
||||
@schedule_bp.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()
|
||||
if "start" in data:
|
||||
event.start = datetime.fromisoformat(data["start"]).replace(tzinfo=timezone.utc)
|
||||
if "description" in data:
|
||||
event.description = data["description"]
|
||||
if "type" in data:
|
||||
event_type = event_controller.get_event_type(data["type"])
|
||||
event.type = event_type
|
||||
event_controller.update()
|
||||
return jsonify(event)
|
||||
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>", methods=["DELETE"])
|
||||
@login_required(permission=permissions.DELETE)
|
||||
def __delete_event(event_id, **kwargs):
|
||||
if not event_controller.delete_event(event_id):
|
||||
raise NotFound
|
||||
db.session.commit()
|
||||
return jsonify({"ok": "ok"})
|
||||
def delete_event(event_id, current_session):
|
||||
"""Delete an event
|
||||
|
||||
Route: ``/events/<event_id>`` | Method: ``DELETE``
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>/slots", methods=["GET"])
|
||||
@login_required()
|
||||
def __get_slots(event_id, **kwargs):
|
||||
event = event_controller.get_event(event_id)
|
||||
if not event:
|
||||
raise NotFound
|
||||
return jsonify({event.slots})
|
||||
Args:
|
||||
event_id: Identifier of the event
|
||||
current_session: Session sent with Authorization Header
|
||||
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["GET"])
|
||||
@login_required()
|
||||
def __get_slot(event_id, slot_id, **kwargs):
|
||||
slot = event_controller.get_event_slot(slot_id, event_id)
|
||||
if slot:
|
||||
return jsonify(slot)
|
||||
raise NotFound
|
||||
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["DELETE"])
|
||||
@login_required(permission=permissions.DELETE)
|
||||
def __delete_slot(event_id, slot_id, **kwargs):
|
||||
if event_controller.delete_event_slot(slot_id, event_id):
|
||||
return jsonify({"ok": "ok"})
|
||||
raise NotFound
|
||||
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["PUT"])
|
||||
@login_required(permission=permissions.EDIT)
|
||||
def __update_slot(event_id, slot_id, **kwargs):
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
raise BadRequest
|
||||
|
||||
for job in data["jobs"]:
|
||||
event_controller.add_job(job.kind, job.user)
|
||||
if event_controller.delete_event_slot(slot_id, event_id):
|
||||
return jsonify({"ok": "ok"})
|
||||
raise NotFound
|
||||
Returns:
|
||||
HTTP-NoContent or HTTP-error
|
||||
"""
|
||||
event_controller.delete_event(event_id)
|
||||
return "", NO_CONTENT
|
||||
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>/slots", methods=["POST"])
|
||||
@login_required(permission=permissions.EDIT)
|
||||
def __add_slot(event_id, **kwargs):
|
||||
def add_event_slot(event_id, current_session):
|
||||
"""Add an new EventSlot to an Event
|
||||
|
||||
Route: ``/events/<event_id>/slots`` | Method: ``POST``
|
||||
|
||||
POST-data: See TS interface for EventSlot
|
||||
|
||||
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)
|
||||
if not event:
|
||||
raise NotFound
|
||||
data = request.get_json()
|
||||
attr = {"job_slots": []}
|
||||
try:
|
||||
if "start" in data:
|
||||
attr["start"] = parser.isoparse(data["start"])
|
||||
if "end" in data:
|
||||
attr["end"] = parser.isoparse(data["end"])
|
||||
for job in data["jobs"]:
|
||||
attr["job_slots"].append({"needed_persons": job["needed_persons"], "kind": job["kind"]})
|
||||
except KeyError:
|
||||
raise BadRequest("Missing data in request")
|
||||
event_controller.add_slot(event, **attr)
|
||||
return jsonify({"ok": "ok"})
|
||||
if not data:
|
||||
raise BadRequest("Missing POST parameters")
|
||||
|
||||
event_controller.add_event_slot(event, **_event_slot_from_data(data))
|
||||
return jsonify(event)
|
||||
|
||||
|
||||
def __edit_event():
|
||||
...
|
||||
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["PUT"])
|
||||
@login_required(permission=permissions.EDIT)
|
||||
def update_event_slot(event_id, slot_id, current_session):
|
||||
"""Update an EventSlot
|
||||
|
||||
Route: ``/events/<event_id>/slots/<slot_id>`` | Method: ``PUT``
|
||||
|
||||
POST-data: See TS interface for EventSlot
|
||||
|
||||
Args:
|
||||
event_id: Identifier of the event
|
||||
slot_id: Identifier of the slot
|
||||
current_session: Session sent with Authorization Header
|
||||
|
||||
Returns:
|
||||
JSON encoded Event object or HTTP-error
|
||||
"""
|
||||
event = event_controller.get_event(event_id)
|
||||
slot = event_controller.get_event_slot(slot_id)
|
||||
if slot not in event.slots:
|
||||
raise NotFound
|
||||
|
||||
data = _event_slot_from_data(request.get_json())
|
||||
slot.start = data["start"]
|
||||
if "end" in data:
|
||||
slot.end = data["end"]
|
||||
event_controller.update()
|
||||
return jsonify(event)
|
||||
|
||||
|
||||
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["DELETE"])
|
||||
@login_required(permission=permissions.EDIT)
|
||||
def delete_event_slot(event_id, slot_id, current_session):
|
||||
"""Delete an EventSlot
|
||||
|
||||
Route: ``/events/<event_id>/slots/<slot_id>`` | Method: ``DELETE``
|
||||
|
||||
Args:
|
||||
event_id: Identifier of the event
|
||||
slot_id: Identifier of the slot
|
||||
current_session: Session sent with Authorization Header
|
||||
|
||||
Returns:
|
||||
JSON encoded Event object or HTTP-error
|
||||
"""
|
||||
event = event_controller.get_event(event_id)
|
||||
event_controller.remove_event_slot(event, slot_id)
|
||||
return jsonify(event)
|
||||
|
||||
|
||||
# TODO: JobSlot, Job!
|
||||
# TODO: JobTransfer
|
||||
|
|
|
@ -6,6 +6,10 @@ from flaschengeist.database import db
|
|||
from flaschengeist.plugins.schedule.models import EventType, Event, EventSlot, JobSlot, JobType
|
||||
|
||||
|
||||
def update():
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def get_event_types():
|
||||
return EventType.query.all()
|
||||
|
||||
|
@ -84,34 +88,41 @@ def delete_job_type(name):
|
|||
raise BadRequest("Type still in use")
|
||||
|
||||
|
||||
def get_event(id):
|
||||
return Event.query.get(id)
|
||||
def get_event(event_id):
|
||||
event = Event.query.get(event_id)
|
||||
if event is None:
|
||||
raise NotFound
|
||||
return event
|
||||
|
||||
|
||||
def get_events(begin, end):
|
||||
def get_events(start, end):
|
||||
"""Query events which start from begin until end
|
||||
Args:
|
||||
begin (datetime): Earliest start
|
||||
start (datetime): Earliest start
|
||||
end (datetime): Latest start
|
||||
|
||||
Returns: collection of Event objects
|
||||
"""
|
||||
return Event.query.filter((begin <= Event.begin), (Event.begin < end))
|
||||
return Event.query.filter((start <= Event.start), (Event.start < end)).all()
|
||||
|
||||
|
||||
def delete_event(id):
|
||||
def delete_event(event_id):
|
||||
"""Delete event with given ID
|
||||
Args:
|
||||
id: id of Event to delete
|
||||
event_id: id of Event to delete
|
||||
|
||||
Returns: True if successful, False if Event is not found
|
||||
Raises:
|
||||
NotFound if not found
|
||||
"""
|
||||
return Event.query.filter(Event.id == id).delete() == 1
|
||||
event = get_event(event_id)
|
||||
db.session.delete(event)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def create_event(begin, kind, end=None, description=None):
|
||||
def create_event(event_type, start, slots=[], description=None):
|
||||
try:
|
||||
event = Event(begin=begin, end=end, description=description, kind=kind)
|
||||
logger.debug(event_type)
|
||||
event = Event(start=start, description=description, type=event_type, slots=slots)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
return event
|
||||
|
@ -120,24 +131,25 @@ def create_event(begin, kind, end=None, description=None):
|
|||
raise BadRequest
|
||||
|
||||
|
||||
def create_job_kind(name):
|
||||
try:
|
||||
kind = JobKind(name=name)
|
||||
db.session.add(kind)
|
||||
db.session.commit()
|
||||
return kind
|
||||
except IntegrityError:
|
||||
logger.debug("IntegrityError: Looks like there is a name collision", exc_info=True)
|
||||
raise BadRequest("Name already exists")
|
||||
def get_event_slot(slot_id):
|
||||
slot = EventSlot.query.get(slot_id)
|
||||
if slot is None:
|
||||
raise NotFound
|
||||
return slot
|
||||
|
||||
|
||||
|
||||
|
||||
def add_slot(event, job_slots, needed_persons, start=None, end=None):
|
||||
event_slot = EventSlot(start=start, end=end)
|
||||
for slot in job_slots:
|
||||
kind = JobKind.query.get(slot.id)
|
||||
job_slot = JobSlot(kind=kind, needed_persons=slot.needed_persons)
|
||||
event_slot.add_slot(job_slot)
|
||||
event.add_slot(event_slot)
|
||||
def add_event_slot(event, start, end=None):
|
||||
event_slot = EventSlot(start=start, end=end, event_=event)
|
||||
if start < event.start:
|
||||
raise BadRequest("Start before event start")
|
||||
db.session.add(event_slot)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def remove_event_slot(event, slot_id):
|
||||
slot = get_event_slot(slot_id)
|
||||
if slot in event.slots:
|
||||
event.slots.remove(slot)
|
||||
else:
|
||||
raise NotFound
|
||||
db.session.commit()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from flaschengeist.models import ModelSerializeMixin
|
||||
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
|
||||
from flaschengeist.models.user import User
|
||||
from flaschengeist.database import db
|
||||
|
||||
|
@ -21,6 +21,7 @@ class JobType(db.Model, ModelSerializeMixin):
|
|||
id_ = db.Column("id", db.Integer, primary_key=True)
|
||||
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||
|
||||
|
||||
########
|
||||
# Jobs #
|
||||
########
|
||||
|
@ -55,6 +56,7 @@ class JobSlot(db.Model, ModelSerializeMixin):
|
|||
|
||||
event_slot_ = db.relationship("EventSlot", back_populates="jobs")
|
||||
|
||||
|
||||
##########
|
||||
# Events #
|
||||
##########
|
||||
|
@ -62,12 +64,13 @@ class JobSlot(db.Model, ModelSerializeMixin):
|
|||
|
||||
class EventSlot(db.Model, ModelSerializeMixin):
|
||||
"""Model for an EventSlot"""
|
||||
|
||||
__tablename__ = "event_slot"
|
||||
_event_id = db.Column("event_id", db.Integer, db.ForeignKey("event.id"), nullable=False)
|
||||
|
||||
id: int = db.Column(db.Integer, primary_key=True)
|
||||
start: datetime = db.Column(db.DateTime)
|
||||
end: Optional[datetime] = db.Column(db.DateTime)
|
||||
start: datetime = db.Column(UtcDateTime)
|
||||
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||
jobs: [JobSlot] = db.relationship("JobSlot", back_populates="event_slot_")
|
||||
|
||||
event_ = db.relationship("Event", back_populates="slots")
|
||||
|
@ -75,12 +78,14 @@ class EventSlot(db.Model, ModelSerializeMixin):
|
|||
|
||||
class Event(db.Model, ModelSerializeMixin):
|
||||
"""Model for an Event"""
|
||||
|
||||
__tablename__ = "event"
|
||||
_type_id = db.Column("type_id", db.Integer, db.ForeignKey("event_type.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
id: int = db.Column(db.Integer, primary_key=True)
|
||||
begin: datetime = db.Column(db.DateTime, nullable=False)
|
||||
end: datetime = db.Column(db.DateTime)
|
||||
description: str = db.Column(db.String(240))
|
||||
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||
description: Optional[str] = db.Column(db.String(240))
|
||||
type: EventType = db.relationship("EventType")
|
||||
slots: [EventSlot] = db.relationship("EventSlot", back_populates="event_", cascade="all, delete")
|
||||
slots: [EventSlot] = db.relationship(
|
||||
"EventSlot", back_populates="event_", cascade="all,delete,delete-orphan", order_by="EventSlot.start"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue