[Plugin] schedule: Working Event, EventSlot, EventType and JobType

This commit is contained in:
Ferdinand Thiessen 2020-11-02 06:30:30 +01:00
parent 363ec6530b
commit 4da4c1ee01
4 changed files with 210 additions and 118 deletions

View File

@ -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

View File

@ -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()

View File

@ -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"
)

View File

@ -18,7 +18,6 @@ setup(
"flask_sqlalchemy",
"flask_cors",
"werkzeug",
"python-dateutil",
# Needed for python < 3.7
"backports-datetime-fromisoformat",
],