[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 datetime import datetime, timedelta, timezone
from http.client import NO_CONTENT, CREATED from http.client import NO_CONTENT, CREATED
from flask import Blueprint, request, jsonify 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.plugins import Plugin
from flaschengeist.models.session import Session
from flaschengeist.decorator import login_required from flaschengeist.decorator import login_required
from flaschengeist.utils.datetime import from_iso_format from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.controller import userController
from . import event_controller, permissions from . import event_controller, permissions
from . import models from . import models
@ -32,7 +34,7 @@ class SchedulePlugin(Plugin):
def get_event_types(current_session): def get_event_types(current_session):
"""Get all EventTypes """Get all EventTypes
Route: ``/event-types`` | Method: ``GET`` Route: ``/schedule/event-types`` | Method: ``GET``
Args: Args:
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
@ -49,7 +51,7 @@ def get_event_types(current_session):
def new_event_type(current_session): def new_event_type(current_session):
"""Create a new EventType """Create a new EventType
Route: ``/event-types`` | Method: ``POST`` Route: ``/schedule/event-types`` | Method: ``POST``
POST-data: ``{name: string}`` POST-data: ``{name: string}``
@ -71,7 +73,7 @@ def new_event_type(current_session):
def modify_event_type(name, current_session): def modify_event_type(name, current_session):
"""Rename or delete an event type """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}`` POST-data: (if renaming) ``{name: string}``
@ -97,7 +99,7 @@ def modify_event_type(name, current_session):
def get_job_types(current_session): def get_job_types(current_session):
"""Get all JobTypes """Get all JobTypes
Route: ``/job-types`` | Method: ``GET`` Route: ``/schedule/job-types`` | Method: ``GET``
Args: Args:
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
@ -114,7 +116,7 @@ def get_job_types(current_session):
def new_job_type(current_session): def new_job_type(current_session):
"""Create a new JobType """Create a new JobType
Route: ``/job-types`` | Method: ``POST`` Route: ``/schedule/job-types`` | Method: ``POST``
POST-data: ``{name: string}`` POST-data: ``{name: string}``
@ -122,38 +124,38 @@ def new_job_type(current_session):
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
Returns: Returns:
HTTP-Created or HTTP-error JSON encoded JobType or HTTP-error
""" """
data = request.get_json() data = request.get_json()
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
event_controller.create_job_type(data["name"]) jt = event_controller.create_job_type(data["name"])
return "", CREATED 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) @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 """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}`` POST-data: (if renaming) ``{name: string}``
Args: Args:
name: Name identifying the JobType type_id: Identifier of the JobType
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
Returns: Returns:
HTTP-NoContent or HTTP-error HTTP-NoContent or HTTP-error
""" """
if request.method == "DELETE": if request.method == "DELETE":
event_controller.delete_name_type(name) event_controller.delete_job_type(type_id)
else: else:
data = request.get_json() data = request.get_json()
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_job_type(type_id, data["name"])
return "", NO_CONTENT return "", NO_CONTENT
@ -162,7 +164,7 @@ def modify_job_type(name, current_session):
def get_event(event_id, current_session): def get_event(event_id, current_session):
"""Get event by id """Get event by id
Route: ``/events/<event_id>`` | Method: ``GET`` Route: ``/schedule/events/<event_id>`` | Method: ``GET``
Args: Args:
event_id: ID identifying the event 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), """Get Event objects for specified date (or month or year),
if nothing set then events for current month are returned 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: Args:
year (int, optional): year to query, defaults to current year 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 JSON encoded list containing events found or HTTP-error
""" """
try: try:
begin = datetime(year=year, month=month, day=1) begin = datetime(year=year, month=month, day=1, tzinfo=timezone.utc)
if day: if day:
begin += timedelta(days=day - 1) begin += timedelta(days=day - 1)
end = begin + timedelta(days=1) end = begin + timedelta(days=1)
else: else:
if month == 12: 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: 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) events = event_controller.get_events(begin, end)
return jsonify(events) return jsonify(events)
@ -226,7 +228,7 @@ def _event_slot_from_data(data):
def create_event(current_session): def create_event(current_session):
"""Create an new event """Create an new event
Route: ``/events`` | Method: ``POST`` Route: ``/schedule/events`` | Method: ``POST``
POST-data: See interfaces for Event, can already contain slots 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): def modify_event(event_id, current_session):
"""Modify an event """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 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): def delete_event(event_id, current_session):
"""Delete an event """Delete an event
Route: ``/events/<event_id>`` | Method: ``DELETE`` Route: ``/schedule/events/<event_id>`` | Method: ``DELETE``
Args: Args:
event_id: Identifier of the event 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): def add_event_slot(event_id, current_session):
"""Add an new EventSlot to an Event """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 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): def update_event_slot(event_id, slot_id, current_session):
"""Update an EventSlot """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 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): def delete_event_slot(event_id, slot_id, current_session):
"""Delete an EventSlot """Delete an EventSlot
Route: ``/events/<event_id>/slots/<slot_id>`` | Method: ``DELETE`` Route: ``/schedule/events/<event_id>/slots/<slot_id>`` | Method: ``DELETE``
Args: Args:
event_id: Identifier of the event event_id: Identifier of the event
@ -375,5 +377,106 @@ def delete_event_slot(event_id, slot_id, current_session):
return jsonify(event) 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 # TODO: JobTransfer

View File

@ -3,7 +3,7 @@ from sqlalchemy.exc import IntegrityError
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.database import db 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(): def update():
@ -53,8 +53,8 @@ def get_job_types():
return JobType.query.all() return JobType.query.all()
def get_job_type(name): def get_job_type(type_id):
job_type = JobType.query.filter(JobType.name == name).one_or_none() job_type = JobType.query.get(type_id)
if not job_type: if not job_type:
raise NotFound raise NotFound
return job_type return job_type
@ -153,3 +153,34 @@ def remove_event_slot(event, slot_id):
else: else:
raise NotFound raise NotFound
db.session.commit() 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 datetime import datetime
from typing import Optional from typing import Optional
from sqlalchemy import UniqueConstraint
from flaschengeist.models import ModelSerializeMixin, UtcDateTime 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
@ -18,7 +20,7 @@ class EventType(db.Model, ModelSerializeMixin):
class JobType(db.Model, ModelSerializeMixin): class JobType(db.Model, ModelSerializeMixin):
__tablename__ = "job_type" __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) 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): class Job(db.Model, ModelSerializeMixin):
__tablename__ = "job" __tablename__ = "job"
userid: str = None userid: str = ""
value: float = db.Column(db.Numeric(precision=3, scale=2), nullable=False) value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
id_ = db.Column("id", db.Integer, primary_key=True) _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) _user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True)
_slot_id = db.Column("slot_id", db.Integer, db.ForeignKey("job_slot.id"), nullable=False)
user_: User = db.relationship("User") user_: User = db.relationship("User")
slot_ = db.relationship("JobSlot") slot_ = db.relationship("JobSlot")
@property @property
def userid(self): def userid(self):
return self._user.userid return self.user_.userid
class JobSlot(db.Model, ModelSerializeMixin): class JobSlot(db.Model, ModelSerializeMixin):
__tablename__ = "job_slot" __tablename__ = "job_slot"
_type_id = db.Column("type_id", db.Integer, db.ForeignKey("job_type.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")) _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") type: JobType = db.relationship("JobType")
users: [Job] = db.relationship("Job", back_populates="slot_") 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") event_slot_ = db.relationship("EventSlot", back_populates="jobs")
__table_args__ = (UniqueConstraint('type_id', 'event_slot_id', name='_type_event_slot_uc'),)
########## ##########
# Events # # Events #