[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 Provides duty schedule / duty roster functions
""" """
import http from datetime import datetime, timedelta, timezone
from http.client import NO_CONTENT, CREATED
from dateutil import parser
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound
from backports.datetime_fromisoformat import MonkeyPatch
from flaschengeist.database import db
from flaschengeist.plugins import Plugin from flaschengeist.plugins import Plugin
from flaschengeist.decorator import login_required from flaschengeist.decorator import login_required
from . import event_controller, permissions from . import event_controller, permissions
from .models import EventType from . import models
MonkeyPatch.patch_fromisoformat()
schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule") schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule")
class SchedulePlugin(Plugin): class SchedulePlugin(Plugin):
models = models
def __init__(self, config): def __init__(self, config):
super().__init__( super().__init__(
blueprint=schedule_bp, blueprint=schedule_bp,
@ -63,7 +64,7 @@ def new_event_type(current_session):
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
event_controller.create_event_type(data["name"]) event_controller.create_event_type(data["name"])
return "", http.HTTPStatus.CREATED return "", CREATED
@schedule_bp.route("/event-types/<name>", methods=["PUT", "DELETE"]) @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: if "name" not in data:
raise BadRequest("Parameter missing in data") raise BadRequest("Parameter missing in data")
event_controller.rename_event_type(name, data["name"]) event_controller.rename_event_type(name, data["name"])
return "", http.HTTPStatus.NO_CONTENT return "", NO_CONTENT
@schedule_bp.route("/job-types", methods=["GET"]) @schedule_bp.route("/job-types", methods=["GET"])
@ -128,7 +129,7 @@ def new_job_type(current_session):
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
event_controller.create_job_type(data["name"]) event_controller.create_job_type(data["name"])
return "", http.HTTPStatus.CREATED return "", CREATED
@schedule_bp.route("/job-types/<name>", methods=["PUT", "DELETE"]) @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: if "name" not in data:
raise BadRequest("Parameter missing in data") raise BadRequest("Parameter missing in data")
event_controller.rename_name_type(name, data["name"]) 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"]) @schedule_bp.route("/events/<int:event_id>", methods=["GET"])
@login_required() @login_required()
def get_event(event_id, current_session): def get_event(event_id, current_session):
@ -173,8 +173,6 @@ def get_event(event_id, current_session):
JSON encoded event object JSON encoded event object
""" """
event = event_controller.get_event(event_id) event = event_controller.get_event(event_id)
if not event:
raise NotFound
return jsonify(event) return jsonify(event)
@ -196,9 +194,6 @@ def get_events(current_session, year=datetime.now().year, month=datetime.now().m
Returns: Returns:
JSON encoded list containing events found or HTTP-error JSON encoded list containing events found or HTTP-error
Raises:
BadRequest: If date is invalid
""" """
try: try:
begin = datetime(year=year, month=month, day=1) 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") 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"]) @schedule_bp.route("/events", methods=["POST"])
@login_required(permission=permissions.CREATE) @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() data = request.get_json()
event = event_controller.create_event( event = event_controller.create_event(
begin=parser.isoparse(data["begin"]), start=start, event_type=event_type, description=data["description"] if "description" in data else None
end=parser.isoparse(data["end"]),
description=data["description"],
type=EventType.query.get(data["kind"]),
) )
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"]) @schedule_bp.route("/events/<int:event_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE) @login_required(permission=permissions.DELETE)
def __delete_event(event_id, **kwargs): def delete_event(event_id, current_session):
if not event_controller.delete_event(event_id): """Delete an event
raise NotFound
db.session.commit()
return jsonify({"ok": "ok"})
Route: ``/events/<event_id>`` | Method: ``DELETE``
@schedule_bp.route("/events/<int:event_id>/slots", methods=["GET"]) Args:
@login_required() event_id: Identifier of the event
def __get_slots(event_id, **kwargs): current_session: Session sent with Authorization Header
event = event_controller.get_event(event_id)
if not event:
raise NotFound
return jsonify({event.slots})
Returns:
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["GET"]) HTTP-NoContent or HTTP-error
@login_required() """
def __get_slot(event_id, slot_id, **kwargs): event_controller.delete_event(event_id)
slot = event_controller.get_event_slot(slot_id, event_id) return "", NO_CONTENT
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
@schedule_bp.route("/events/<int:event_id>/slots", methods=["POST"]) @schedule_bp.route("/events/<int:event_id>/slots", methods=["POST"])
@login_required(permission=permissions.EDIT) @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) event = event_controller.get_event(event_id)
if not event:
raise NotFound
data = request.get_json() data = request.get_json()
attr = {"job_slots": []} if not data:
try: raise BadRequest("Missing POST parameters")
if "start" in data:
attr["start"] = parser.isoparse(data["start"]) event_controller.add_event_slot(event, **_event_slot_from_data(data))
if "end" in data: return jsonify(event)
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"})
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 from flaschengeist.plugins.schedule.models import EventType, Event, EventSlot, JobSlot, JobType
def update():
db.session.commit()
def get_event_types(): def get_event_types():
return EventType.query.all() return EventType.query.all()
@ -84,34 +88,41 @@ def delete_job_type(name):
raise BadRequest("Type still in use") raise BadRequest("Type still in use")
def get_event(id): def get_event(event_id):
return Event.query.get(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 """Query events which start from begin until end
Args: Args:
begin (datetime): Earliest start start (datetime): Earliest start
end (datetime): Latest start end (datetime): Latest start
Returns: collection of Event objects 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 """Delete event with given ID
Args: 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: 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.add(event)
db.session.commit() db.session.commit()
return event return event
@ -120,24 +131,25 @@ def create_event(begin, kind, end=None, description=None):
raise BadRequest raise BadRequest
def create_job_kind(name): def get_event_slot(slot_id):
try: slot = EventSlot.query.get(slot_id)
kind = JobKind(name=name) if slot is None:
db.session.add(kind) raise NotFound
db.session.commit() return slot
return kind
except IntegrityError:
logger.debug("IntegrityError: Looks like there is a name collision", exc_info=True)
raise BadRequest("Name already exists")
def add_event_slot(event, start, end=None):
event_slot = EventSlot(start=start, end=end, event_=event)
def add_slot(event, job_slots, needed_persons, start=None, end=None): if start < event.start:
event_slot = EventSlot(start=start, end=end) raise BadRequest("Start before event start")
for slot in job_slots: db.session.add(event_slot)
kind = JobKind.query.get(slot.id) db.session.commit()
job_slot = JobSlot(kind=kind, needed_persons=slot.needed_persons)
event_slot.add_slot(job_slot)
event.add_slot(event_slot) 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() db.session.commit()

View File

@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from flaschengeist.models import ModelSerializeMixin from flaschengeist.models import ModelSerializeMixin, UtcDateTime
from flaschengeist.models.user import User from flaschengeist.models.user import User
from flaschengeist.database import db from flaschengeist.database import db
@ -21,6 +21,7 @@ class JobType(db.Model, ModelSerializeMixin):
id_ = db.Column("id", db.Integer, primary_key=True) id_ = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True) name: str = db.Column(db.String(30), nullable=False, unique=True)
######## ########
# Jobs # # Jobs #
######## ########
@ -55,6 +56,7 @@ class JobSlot(db.Model, ModelSerializeMixin):
event_slot_ = db.relationship("EventSlot", back_populates="jobs") event_slot_ = db.relationship("EventSlot", back_populates="jobs")
########## ##########
# Events # # Events #
########## ##########
@ -62,12 +64,13 @@ class JobSlot(db.Model, ModelSerializeMixin):
class EventSlot(db.Model, ModelSerializeMixin): class EventSlot(db.Model, ModelSerializeMixin):
"""Model for an EventSlot""" """Model for an EventSlot"""
__tablename__ = "event_slot" __tablename__ = "event_slot"
_event_id = db.Column("event_id", db.Integer, db.ForeignKey("event.id"), nullable=False) _event_id = db.Column("event_id", db.Integer, db.ForeignKey("event.id"), nullable=False)
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
start: datetime = db.Column(db.DateTime) start: datetime = db.Column(UtcDateTime)
end: Optional[datetime] = db.Column(db.DateTime) end: Optional[datetime] = db.Column(UtcDateTime)
jobs: [JobSlot] = db.relationship("JobSlot", back_populates="event_slot_") jobs: [JobSlot] = db.relationship("JobSlot", back_populates="event_slot_")
event_ = db.relationship("Event", back_populates="slots") event_ = db.relationship("Event", back_populates="slots")
@ -75,12 +78,14 @@ class EventSlot(db.Model, ModelSerializeMixin):
class Event(db.Model, ModelSerializeMixin): class Event(db.Model, ModelSerializeMixin):
"""Model for an Event""" """Model for an Event"""
__tablename__ = "event" __tablename__ = "event"
_type_id = db.Column("type_id", db.Integer, db.ForeignKey("event_type.id", ondelete="CASCADE"), nullable=False) _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) id: int = db.Column(db.Integer, primary_key=True)
begin: datetime = db.Column(db.DateTime, nullable=False) start: datetime = db.Column(UtcDateTime, nullable=False)
end: datetime = db.Column(db.DateTime) description: Optional[str] = db.Column(db.String(240))
description: str = db.Column(db.String(240))
type: EventType = db.relationship("EventType") 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_sqlalchemy",
"flask_cors", "flask_cors",
"werkzeug", "werkzeug",
"python-dateutil",
# Needed for python < 3.7 # Needed for python < 3.7
"backports-datetime-fromisoformat", "backports-datetime-fromisoformat",
], ],