diff --git a/flaschengeist/plugins/events/event_controller.py b/flaschengeist/plugins/events/event_controller.py index 31a3b7b..d4c61c8 100644 --- a/flaschengeist/plugins/events/event_controller.py +++ b/flaschengeist/plugins/events/event_controller.py @@ -1,31 +1,82 @@ from datetime import datetime, timedelta, timezone from typing import Optional -from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.exceptions import BadRequest, NotFound, Conflict from sqlalchemy.exc import IntegrityError from flaschengeist import logger from flaschengeist.database import db from flaschengeist.plugins.events import EventPlugin -from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, Service +from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, JobTransfer, Service from flaschengeist.utils.scheduler import scheduled +TRANSFER_REQUEST = 0x10 +TRANSFER_ACCEPTED = 0x11 +TRANSFER_REJECTED = 0x12 + + def update(): db.session.commit() +def invite(job, invitee, sender): + EventPlugin.plugin.notify(invitee, "Neue Diensteinladung", {"type": 0, "sender": sender.userid, "job": job.id}) + + +def get_transfer(id): + return JobTransfer.query.get(id) + + +def transfer(job: Job, new, old): + service = Service.query.get((job.id, old.id_)) + if service is None: + raise BadRequest + + jt = JobTransfer(old_user=old, new_user=new, job=job) + db.session.add(jt) + db.session.commit() + EventPlugin.plugin.notify( + new, + "Neue Dienstübergabe", + {"type": TRANSFER_REQUEST, "sender": old.userid, "event": job.event_id_, "job": job.id, "id": jt.id}, + ) + + +def accept_transfer(jt: JobTransfer, accept=True): + try: + if accept: + service = Service.query.get((jt.job.id, jt.old_user.id_)) + if service is not None: + service.user_ = jt.new_user + else: + raise Conflict + EventPlugin.plugin.notify( + jt.old_user, + "Dienstübergabe akzeptiert", + {"type": TRANSFER_ACCEPTED, "sender": jt.new_user.userid, "event": jt.job.event_id_, "job": jt.job_id_}, + ) + else: + EventPlugin.plugin.notify( + jt.old_user, + "Dienstübergabe abgelehnt", + {"type": TRANSFER_REJECTED, "sender": jt.new_user.userid, "event": jt.job.event_id_, "job": jt.job_id_}, + ) + + finally: + db.session.delete(jt) + db.session.commit() + + def get_event_types(): return EventType.query.all() def get_event_type(identifier): - """Get EventType by ID (int) or name (string)""" + """Get EventType by ID (int)""" if isinstance(identifier, int): et = EventType.query.get(identifier) - elif isinstance(identifier, str): - et = EventType.query.filter(EventType.name == identifier).one_or_none() else: logger.debug("Invalid identifier type for EventType") raise BadRequest @@ -102,11 +153,11 @@ def delete_job_type(name): raise BadRequest("Type still in use") -def clear_backup(event: Event): +def clear_backup(event: Event, backup): for job in event.jobs: services = [] for service in job.services: - if not service.is_backup: + if not service.is_backup or service.userid == backup: services.append(service) job.services = services @@ -115,8 +166,6 @@ def get_event(event_id, with_backup=False) -> Event: event = Event.query.get(event_id) if event is None: raise NotFound - if not with_backup: - return clear_backup(event) return event @@ -139,9 +188,9 @@ def get_events(start: Optional[datetime] = None, end=None, with_backup=False): if end is not None: query = query.filter(Event.start < end) events = query.all() - if not with_backup: + if not (with_backup is True): for event in events: - clear_backup(event) + clear_backup(event, with_backup) return events @@ -205,7 +254,7 @@ def delete_job(job: Job): db.session.commit() -def assign_to_job(job: Job, user, value): +def assign_to_job(job: Job, user, value, backup=False): service = Service.query.get((job.id, user.id_)) if value < 0: if not service: @@ -214,8 +263,9 @@ def assign_to_job(job: Job, user, value): else: if service: service.value = value + service.is_backup = backup else: - service = Service(user_=user, value=value, job_=job) + service = Service(user_=user, value=value, job_=job, is_backup=backup) db.session.add(service) db.session.commit() diff --git a/flaschengeist/plugins/events/models.py b/flaschengeist/plugins/events/models.py index daf308a..cf2474c 100644 --- a/flaschengeist/plugins/events/models.py +++ b/flaschengeist/plugins/events/models.py @@ -59,7 +59,9 @@ class Job(db.Model, ModelSerializeMixin): end: Optional[datetime] = db.Column(UtcDateTime) type: Union[JobType, int] = db.relationship("JobType") comment: Optional[str] = db.Column(db.String(256)) - services: list[Service] = db.relationship("Service", back_populates="job_") + services: list[Service] = db.relationship( + "Service", back_populates="job_", cascade="save-update, merge, delete, delete-orphan" + ) required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False) event_ = db.relationship("Event", back_populates="jobs") @@ -68,6 +70,17 @@ class Job(db.Model, ModelSerializeMixin): __table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),) +class JobTransfer(db.Model, ModelSerializeMixin): + __tablename__ = _table_prefix_ + "job_transfer" + id: int = db.Column(Serial, primary_key=True) + old_id_ = db.Column("old_id", Serial, db.ForeignKey("user.id"), nullable=False) + new_id_ = db.Column("new_id", Serial, db.ForeignKey("user.id"), nullable=False) + job_id_ = db.Column("job_id", Serial, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False) + old_user = db.relationship("User", foreign_keys=[old_id_]) + new_user = db.relationship("User", foreign_keys=[new_id_]) + job = db.relationship("Job") + + ########## # Events # ########## diff --git a/flaschengeist/plugins/events/routes.py b/flaschengeist/plugins/events/routes.py index 7fccccf..77aeaa7 100644 --- a/flaschengeist/plugins/events/routes.py +++ b/flaschengeist/plugins/events/routes.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime from http.client import NO_CONTENT from flask import request, jsonify from werkzeug.exceptions import BadRequest, NotFound, Forbidden @@ -9,6 +9,7 @@ from flaschengeist.utils.datetime import from_iso_format from flaschengeist.controller import userController from . import event_controller, permissions, EventPlugin +from ... import logger from ...utils.HTTP import no_content @@ -169,14 +170,15 @@ def get_event(event_id, current_session): JSON encoded event object """ event = event_controller.get_event( - event_id, with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP) + event_id, + with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP) or current_session.user_.userid, ) return jsonify(event) @EventPlugin.blueprint.route("/events", methods=["GET"]) @login_required() -def get_filtered_events(current_session): +def get_events(current_session): begin = request.args.get("from") if begin is not None: begin = from_iso_format(begin) @@ -187,48 +189,13 @@ def get_filtered_events(current_session): begin = datetime.now() return jsonify( event_controller.get_events( - begin, end, with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP) + begin, + end, + with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP) or current_session.user_.userid, ) ) -@EventPlugin.blueprint.route("/events//", methods=["GET"]) -@EventPlugin.blueprint.route("/events///", methods=["GET"]) -@login_required() -def get_events(current_session, year=datetime.now().year, month=datetime.now().month, day=None): - """Get Event objects for specified date (or month or year), - if nothing set then events for current month are returned - - Route: ``/events[//[/]]`` | Method: ``GET`` - - Args: - year (int, optional): year to query, defaults to current year - month (int, optional): month to query (if set), defaults to current month - day (int, optional): day to query events for (if set) - current_session: Session sent with Authorization Header - - Returns: - JSON encoded list containing events found or HTTP-error - """ - try: - 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, tzinfo=timezone.utc) - else: - end = datetime(year=year, month=month + 1, day=1, tzinfo=timezone.utc) - - events = event_controller.get_events( - begin, end, with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP) - ) - return jsonify(events) - except ValueError: - raise BadRequest("Invalid date given") - - def _add_job(event, data): try: start = from_iso_format(data["start"]) @@ -316,8 +283,10 @@ def modify_event(event_id, current_session): 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_type = data["type"] + if isinstance(event_type, dict) and "id" in event_type: + event_type = data["type"]["id"] + event.type = event_controller.get_event_type(event_type) event_controller.update() return jsonify(event) @@ -361,12 +330,12 @@ def add_job(event_id, current_session): return jsonify(event) -@EventPlugin.blueprint.route("/events//jobs/", methods=["DELETE"]) +@EventPlugin.blueprint.route("/events//", methods=["DELETE"]) @login_required(permission=permissions.DELETE) def delete_job(event_id, job_id, current_session): """Delete a Job - Route: ``/events//jobs/`` | Method: ``DELETE`` + Route: ``/events//`` | Method: ``DELETE`` Args: event_id: Identifier of the event @@ -381,12 +350,12 @@ def delete_job(event_id, job_id, current_session): return no_content() -@EventPlugin.blueprint.route("/events//jobs/", methods=["PUT"]) -@login_required() +@EventPlugin.blueprint.route("/events//", methods=["PUT"]) +@login_required(permission=permissions.ASSIGN) def update_job(event_id, job_id, current_session: Session): """Edit Job or assign user to the Job - Route: ``/events//jobs/`` | Method: ``PUT`` + Route: ``/events//`` | Method: ``PUT`` POST-data: See TS interface for Job or ``{user: {userid: string, value: number}}`` @@ -401,31 +370,52 @@ def update_job(event_id, job_id, current_session: Session): job = event_controller.get_job(job_id, event_id) data = request.get_json() - if not data: + if not data or ("user" not in data and "required_services" not in data): raise BadRequest - if ("user" not in data or len(data) > 1) and not current_session.user_.has_permission(permissions.EDIT): - raise Forbidden + # Check if number of services changed, check permission if so + if "required_services" in data: + if not current_session.user_.has_permission(permissions.EDIT): + raise Forbidden + job.required_services = data["required_services"] 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_to_job(job, user, value) - except (KeyError, ValueError): + value = data["user"].get("value", None) + replace = data["user"].get("replace", None) + if replace is not None: + replace = userController.get_user(replace) + event_controller.transfer(job, user, replace) + else: + if value is not None: + if user != current_session.user_ and not current_session.user_.has_permission( + permissions.ASSIGN_OTHER + ): + raise Forbidden + event_controller.assign_to_job(job, user, value, data["user"].get("is_backup", False)) + else: + logger.debug("Invite user") + event_controller.invite(job, user, current_session.user_) + except (KeyError, ValueError, NotFound): raise BadRequest - if "required_services" in data: - job.required_services = data["required_services"] - if "type" in data: - job.type = event_controller.get_job_type(data["type"]) event_controller.update() - return jsonify(job) -# TODO: JobTransfer +@EventPlugin.blueprint.route("/events/transfer/", methods=["PUT", "DELETE"]) +@login_required(permission=permissions.ASSIGN) +def transfer_accepted(transfer_id, current_session: Session): + jt = event_controller.get_transfer(transfer_id) + if jt is None: + raise NotFound + if request.method == "PUT": + if jt.new_user.userid != current_session.userid: + raise Forbidden + event_controller.accept_transfer(jt) + else: + if jt.new_user.userid != current_session.userid or jt.old_user.userid != current_session.userid: + raise Forbidden + event_controller.accept_transfer(jt, False) + return no_content()