[Plugin] schedule: Basics work (all models)

This commit is contained in:
Ferdinand Thiessen 2020-11-02 15:44:43 +01:00
parent ac1189ecaa
commit 5a8a4aa23d
3 changed files with 178 additions and 41 deletions

View File

@ -5,11 +5,13 @@ Provides duty schedule / duty roster functions
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 werkzeug.exceptions import BadRequest, NotFound, Forbidden
from flaschengeist.plugins import Plugin
from flaschengeist.models.session import Session
from flaschengeist.decorator import login_required
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.controller import userController
from . import event_controller, permissions
from . import models
@ -32,7 +34,7 @@ class SchedulePlugin(Plugin):
def get_event_types(current_session):
"""Get all EventTypes
Route: ``/event-types`` | Method: ``GET``
Route: ``/schedule/event-types`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
@ -49,7 +51,7 @@ def get_event_types(current_session):
def new_event_type(current_session):
"""Create a new EventType
Route: ``/event-types`` | Method: ``POST``
Route: ``/schedule/event-types`` | Method: ``POST``
POST-data: ``{name: string}``
@ -71,7 +73,7 @@ def new_event_type(current_session):
def modify_event_type(name, current_session):
"""Rename or delete an event type
Route: ``/event-types/<name>`` | Method: ``PUT`` or ``DELETE``
Route: ``/schedule/event-types/<name>`` | Method: ``PUT`` or ``DELETE``
POST-data: (if renaming) ``{name: string}``
@ -97,7 +99,7 @@ def modify_event_type(name, current_session):
def get_job_types(current_session):
"""Get all JobTypes
Route: ``/job-types`` | Method: ``GET``
Route: ``/schedule/job-types`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
@ -114,7 +116,7 @@ def get_job_types(current_session):
def new_job_type(current_session):
"""Create a new JobType
Route: ``/job-types`` | Method: ``POST``
Route: ``/schedule/job-types`` | Method: ``POST``
POST-data: ``{name: string}``
@ -122,38 +124,38 @@ def new_job_type(current_session):
current_session: Session sent with Authorization Header
Returns:
HTTP-Created or HTTP-error
JSON encoded JobType or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
event_controller.create_job_type(data["name"])
return "", CREATED
jt = event_controller.create_job_type(data["name"])
return jsonify(jt)
@schedule_bp.route("/job-types/<name>", methods=["PUT", "DELETE"])
@schedule_bp.route("/job-types/<int:type_id>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.JOB_TYPE)
def modify_job_type(name, current_session):
def modify_job_type(type_id, current_session):
"""Rename or delete a JobType
Route: ``/job-types/<name>`` | Method: ``PUT`` or ``DELETE``
Route: ``/schedule/job-types/<name>`` | Method: ``PUT`` or ``DELETE``
POST-data: (if renaming) ``{name: string}``
Args:
name: Name identifying the JobType
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_name_type(name)
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_name_type(name, data["name"])
event_controller.rename_job_type(type_id, data["name"])
return "", NO_CONTENT
@ -162,7 +164,7 @@ def modify_job_type(name, current_session):
def get_event(event_id, current_session):
"""Get event by id
Route: ``/events/<event_id>`` | Method: ``GET``
Route: ``/schedule/events/<event_id>`` | Method: ``GET``
Args:
event_id: ID identifying the event
@ -183,7 +185,7 @@ def get_events(current_session, year=datetime.now().year, month=datetime.now().m
"""Get Event objects for specified date (or month or year),
if nothing set then events for current month are returned
Route: ``/events[/<year>/<month>[/<int:day>]]`` | Method: ``GET``
Route: ``/schedule/events[/<year>/<month>[/<int:day>]]`` | Method: ``GET``
Args:
year (int, optional): year to query, defaults to current year
@ -195,15 +197,15 @@ def get_events(current_session, year=datetime.now().year, month=datetime.now().m
JSON encoded list containing events found or HTTP-error
"""
try:
begin = datetime(year=year, month=month, day=1)
begin = datetime(year=year, month=month, day=1, tzinfo=timezone.utc)
if day:
begin += timedelta(days=day - 1)
end = begin + timedelta(days=1)
else:
if month == 12:
end = datetime(year=year + 1, month=1, day=1)
end = datetime(year=year + 1, month=1, day=1, tzinfo=timezone.utc)
else:
end = datetime(year=year, month=month + 1, day=1)
end = datetime(year=year, month=month + 1, day=1, tzinfo=timezone.utc)
events = event_controller.get_events(begin, end)
return jsonify(events)
@ -226,7 +228,7 @@ def _event_slot_from_data(data):
def create_event(current_session):
"""Create an new event
Route: ``/events`` | Method: ``POST``
Route: ``/schedule/events`` | Method: ``POST``
POST-data: See interfaces for Event, can already contain slots
@ -258,7 +260,7 @@ def create_event(current_session):
def modify_event(event_id, current_session):
"""Modify an event
Route: ``/events/<event_id>`` | Method: ``PUT``
Route: ``/schedule/events/<event_id>`` | Method: ``PUT``
POST-data: See interfaces for Event, can already contain slots
@ -287,7 +289,7 @@ def modify_event(event_id, current_session):
def delete_event(event_id, current_session):
"""Delete an event
Route: ``/events/<event_id>`` | Method: ``DELETE``
Route: ``/schedule/events/<event_id>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
@ -305,7 +307,7 @@ def delete_event(event_id, current_session):
def add_event_slot(event_id, current_session):
"""Add an new EventSlot to an Event
Route: ``/events/<event_id>/slots`` | Method: ``POST``
Route: ``/schedule/events/<event_id>/slots`` | Method: ``POST``
POST-data: See TS interface for EventSlot
@ -330,7 +332,7 @@ def add_event_slot(event_id, current_session):
def update_event_slot(event_id, slot_id, current_session):
"""Update an EventSlot
Route: ``/events/<event_id>/slots/<slot_id>`` | Method: ``PUT``
Route: ``/schedule/events/<event_id>/slots/<slot_id>`` | Method: ``PUT``
POST-data: See TS interface for EventSlot
@ -360,7 +362,7 @@ def update_event_slot(event_id, slot_id, current_session):
def delete_event_slot(event_id, slot_id, current_session):
"""Delete an EventSlot
Route: ``/events/<event_id>/slots/<slot_id>`` | Method: ``DELETE``
Route: ``/schedule/events/<event_id>/slots/<slot_id>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
@ -375,5 +377,106 @@ def delete_event_slot(event_id, slot_id, current_session):
return jsonify(event)
# TODO: JobSlot, Job!
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>/jobs", methods=["POST"])
@login_required(permission=permissions.EDIT)
def add_job_slot(event_id, slot_id, current_session):
"""Add an new JobSlot to an Event / EventSlot
Route: ``/schedule/events/<event_id>/slots/<slot_id>/jobs`` | Method: ``POST``
POST-data: ``{type: string, required_jobs: number}``
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 = request.get_json()
try:
job_type = event_controller.get_job_type(data["type"])
required_jobs = data["required_jobs"]
except (KeyError, ValueError):
raise BadRequest("Missing POST parameters")
event_controller.add_job_slot(slot, job_type, required_jobs)
return jsonify(event)
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>/jobs/<int:job_type>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_job_slot(event_id, slot_id, job_type, current_session):
"""Delete a JobSlot
Route: ``/schedule/events/<event_id>/slots/<slot_id>/jobs/<job_type>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
slot_id: Identifier of the EventSlot
job_type: Identifier of the JobSlot
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
event = event_controller.get_event(event_id)
job_slot = event_controller.get_job_slot(slot_id, job_type)
event_controller.delete_job_slot(job_slot)
return jsonify(event)
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>/jobs/<int:job_type>", methods=["PUT"])
@login_required()
def update_job_slot(event_id, slot_id, job_type, current_session: Session):
"""Edit JobSlot or add user to the Slot
Route: ``/schedule/events/<event_id>/slots/<slot_id>/jobs/<job_type>`` | Method: ``PUT``
POST-data: See TS interface for EventSlot or ``{user: {userid: string, value: number}}``
Args:
event_id: Identifier of the event
slot_id: Identifier of the slot
job_type: Identifier of the JobSlot
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_job_slot(slot_id, job_type)
data = request.get_json()
if not data:
raise BadRequest
if ("user" not in data or len(data) > 1) and not current_session._user.has_permission(permissions.EDIT):
raise Forbidden
if "user" in data:
try:
user = userController.get_user(data["user"]["userid"])
value = data["user"]["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
event_controller.assign_job(slot, user, value)
except (KeyError, ValueError):
raise BadRequest
if "required_jobs" in data:
slot.required_jobs = data["required_jobs"]
if "type" in data:
slot.type = event_controller.get_job_type(data["type"])
event_controller.update()
return jsonify(event)
# TODO: JobTransfer

View File

@ -3,7 +3,7 @@ from sqlalchemy.exc import IntegrityError
from flaschengeist import logger
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, Job
def update():
@ -53,8 +53,8 @@ def get_job_types():
return JobType.query.all()
def get_job_type(name):
job_type = JobType.query.filter(JobType.name == name).one_or_none()
def get_job_type(type_id):
job_type = JobType.query.get(type_id)
if not job_type:
raise NotFound
return job_type
@ -153,3 +153,34 @@ def remove_event_slot(event, slot_id):
else:
raise NotFound
db.session.commit()
def get_job_slot(event_slot_id, job_type):
jt = JobSlot.query.filter(JobSlot._type_id == job_type).filter(JobSlot._event_slot_id == event_slot_id).one_or_none()
if jt is None:
raise NotFound
return jt
def add_job_slot(event_slot, job_type, required_jobs):
job_slot = JobSlot(type=job_type, required_jobs=required_jobs, event_slot_=event_slot)
try:
db.session.add(job_slot)
db.session.commit()
except IntegrityError:
raise BadRequest("JobSlot with that type already exists on this EventSlot")
def delete_job_slot(job_slot):
db.session.delete(job_slot)
db.session.commit()
def assign_job(job_slot, user, value):
job = Job.query.get((job_slot.id_, user._id))
if job:
job.value = value
else:
job = Job(user_=user, value=value, slot_=job_slot)
db.session.add(job)
db.session.commit()

View File

@ -1,6 +1,8 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import UniqueConstraint
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
from flaschengeist.models.user import User
from flaschengeist.database import db
@ -18,7 +20,7 @@ class EventType(db.Model, ModelSerializeMixin):
class JobType(db.Model, ModelSerializeMixin):
__tablename__ = "job_type"
id_ = db.Column("id", db.Integer, primary_key=True)
id: int = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
@ -29,33 +31,34 @@ class JobType(db.Model, ModelSerializeMixin):
class Job(db.Model, ModelSerializeMixin):
__tablename__ = "job"
userid: str = None
value: float = db.Column(db.Numeric(precision=3, scale=2), nullable=False)
userid: str = ""
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
id_ = db.Column("id", db.Integer, primary_key=True)
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
_slot_id = db.Column("slot_id", db.Integer, db.ForeignKey("job_slot.id"), nullable=False)
_slot_id = db.Column("slot_id", db.Integer, db.ForeignKey("job_slot.id"), nullable=False, primary_key=True)
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True)
user_: User = db.relationship("User")
slot_ = db.relationship("JobSlot")
@property
def userid(self):
return self._user.userid
return self.user_.userid
class JobSlot(db.Model, ModelSerializeMixin):
__tablename__ = "job_slot"
_type_id = db.Column("type_id", db.Integer, db.ForeignKey("job_type.id"))
_event_slot_id = db.Column("event_slot_id", db.Integer, db.ForeignKey("event_slot.id"))
_type_id = db.Column("type_id", db.Integer, db.ForeignKey("job_type.id"), nullable=False)
_event_slot_id = db.Column("event_slot_id", db.Integer, db.ForeignKey("event_slot.id"), nullable=False)
id: int = db.Column(db.Integer, primary_key=True)
id_: int = db.Column("id", db.Integer, primary_key=True)
type: JobType = db.relationship("JobType")
users: [Job] = db.relationship("Job", back_populates="slot_")
required_jobs: float = db.Column(db.Numeric(precision=4, scale=2))
required_jobs: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False))
event_slot_ = db.relationship("EventSlot", back_populates="jobs")
__table_args__ = (UniqueConstraint('type_id', 'event_slot_id', name='_type_event_slot_uc'),)
##########
# Events #