diff --git a/flaschengeist/plugins/schedule/__init__.py b/flaschengeist/plugins/schedule/__init__.py index 169d836..38cb80a 100644 --- a/flaschengeist/plugins/schedule/__init__.py +++ b/flaschengeist/plugins/schedule/__init__.py @@ -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/`` | Method: ``PUT`` or ``DELETE`` + Route: ``/schedule/event-types/`` | 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/", methods=["PUT", "DELETE"]) +@schedule_bp.route("/job-types/", 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/`` | Method: ``PUT`` or ``DELETE`` + Route: ``/schedule/job-types/`` | 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/`` | Method: ``GET`` + Route: ``/schedule/events/`` | 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[//[/]]`` | Method: ``GET`` + Route: ``/schedule/events[//[/]]`` | 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/`` | Method: ``PUT`` + Route: ``/schedule/events/`` | 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/`` | Method: ``DELETE`` + Route: ``/schedule/events/`` | 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//slots`` | Method: ``POST`` + Route: ``/schedule/events//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//slots/`` | Method: ``PUT`` + Route: ``/schedule/events//slots/`` | 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//slots/`` | Method: ``DELETE`` + Route: ``/schedule/events//slots/`` | 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//slots//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//slots//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//slots//jobs/", 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//slots//jobs/`` | 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//slots//jobs/", 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//slots//jobs/`` | 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 diff --git a/flaschengeist/plugins/schedule/event_controller.py b/flaschengeist/plugins/schedule/event_controller.py index 2ae486c..0124ab5 100644 --- a/flaschengeist/plugins/schedule/event_controller.py +++ b/flaschengeist/plugins/schedule/event_controller.py @@ -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() diff --git a/flaschengeist/plugins/schedule/models.py b/flaschengeist/plugins/schedule/models.py index 8122087..e015243 100644 --- a/flaschengeist/plugins/schedule/models.py +++ b/flaschengeist/plugins/schedule/models.py @@ -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 #