Compare commits

...

1 Commits

Author SHA1 Message Date
Ferdinand Thiessen ce074bb51a [events] Initial work for job transfer 2021-04-04 21:47:04 +02:00
3 changed files with 131 additions and 78 deletions

View File

@ -1,31 +1,82 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound, Conflict
from sqlalchemy.exc import IntegrityError 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.events import EventPlugin 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 from flaschengeist.utils.scheduler import scheduled
TRANSFER_REQUEST = 0x10
TRANSFER_ACCEPTED = 0x11
TRANSFER_REJECTED = 0x12
def update(): def update():
db.session.commit() 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(): def get_event_types():
return EventType.query.all() return EventType.query.all()
def get_event_type(identifier): def get_event_type(identifier):
"""Get EventType by ID (int) or name (string)""" """Get EventType by ID (int)"""
if isinstance(identifier, int): if isinstance(identifier, int):
et = EventType.query.get(identifier) et = EventType.query.get(identifier)
elif isinstance(identifier, str):
et = EventType.query.filter(EventType.name == identifier).one_or_none()
else: else:
logger.debug("Invalid identifier type for EventType") logger.debug("Invalid identifier type for EventType")
raise BadRequest raise BadRequest
@ -102,11 +153,11 @@ def delete_job_type(name):
raise BadRequest("Type still in use") raise BadRequest("Type still in use")
def clear_backup(event: Event): def clear_backup(event: Event, backup):
for job in event.jobs: for job in event.jobs:
services = [] services = []
for service in job.services: for service in job.services:
if not service.is_backup: if not service.is_backup or service.userid == backup:
services.append(service) services.append(service)
job.services = services job.services = services
@ -115,8 +166,6 @@ def get_event(event_id, with_backup=False) -> Event:
event = Event.query.get(event_id) event = Event.query.get(event_id)
if event is None: if event is None:
raise NotFound raise NotFound
if not with_backup:
return clear_backup(event)
return event return event
@ -139,9 +188,9 @@ def get_events(start: Optional[datetime] = None, end=None, with_backup=False):
if end is not None: if end is not None:
query = query.filter(Event.start < end) query = query.filter(Event.start < end)
events = query.all() events = query.all()
if not with_backup: if not (with_backup is True):
for event in events: for event in events:
clear_backup(event) clear_backup(event, with_backup)
return events return events
@ -205,7 +254,7 @@ def delete_job(job: Job):
db.session.commit() 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_)) service = Service.query.get((job.id, user.id_))
if value < 0: if value < 0:
if not service: if not service:
@ -214,8 +263,9 @@ def assign_to_job(job: Job, user, value):
else: else:
if service: if service:
service.value = value service.value = value
service.is_backup = backup
else: 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.add(service)
db.session.commit() db.session.commit()

View File

@ -59,7 +59,9 @@ class Job(db.Model, ModelSerializeMixin):
end: Optional[datetime] = db.Column(UtcDateTime) end: Optional[datetime] = db.Column(UtcDateTime)
type: Union[JobType, int] = db.relationship("JobType") type: Union[JobType, int] = db.relationship("JobType")
comment: Optional[str] = db.Column(db.String(256)) 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) required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
event_ = db.relationship("Event", back_populates="jobs") 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"),) __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 # # Events #
########## ##########

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import datetime
from http.client import NO_CONTENT from http.client import NO_CONTENT
from flask import request, jsonify from flask import request, jsonify
from werkzeug.exceptions import BadRequest, NotFound, Forbidden 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 flaschengeist.controller import userController
from . import event_controller, permissions, EventPlugin from . import event_controller, permissions, EventPlugin
from ... import logger
from ...utils.HTTP import no_content from ...utils.HTTP import no_content
@ -169,14 +170,15 @@ def get_event(event_id, current_session):
JSON encoded event object JSON encoded event object
""" """
event = event_controller.get_event( 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) return jsonify(event)
@EventPlugin.blueprint.route("/events", methods=["GET"]) @EventPlugin.blueprint.route("/events", methods=["GET"])
@login_required() @login_required()
def get_filtered_events(current_session): def get_events(current_session):
begin = request.args.get("from") begin = request.args.get("from")
if begin is not None: if begin is not None:
begin = from_iso_format(begin) begin = from_iso_format(begin)
@ -187,48 +189,13 @@ def get_filtered_events(current_session):
begin = datetime.now() begin = datetime.now()
return jsonify( return jsonify(
event_controller.get_events( 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/<int:year>/<int:month>", methods=["GET"])
@EventPlugin.blueprint.route("/events/<int:year>/<int:month>/<int:day>", 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[/<year>/<month>[/<int:day>]]`` | 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): def _add_job(event, data):
try: try:
start = from_iso_format(data["start"]) start = from_iso_format(data["start"])
@ -316,8 +283,10 @@ def modify_event(event_id, current_session):
if "description" in data: if "description" in data:
event.description = data["description"] event.description = data["description"]
if "type" in data: if "type" in data:
event_type = event_controller.get_event_type(data["type"]) event_type = data["type"]
event.type = event_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() event_controller.update()
return jsonify(event) return jsonify(event)
@ -361,12 +330,12 @@ def add_job(event_id, current_session):
return jsonify(event) return jsonify(event)
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"]) @EventPlugin.blueprint.route("/events/<int:event_id>/<int:job_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE) @login_required(permission=permissions.DELETE)
def delete_job(event_id, job_id, current_session): def delete_job(event_id, job_id, current_session):
"""Delete a Job """Delete a Job
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``DELETE`` Route: ``/events/<event_id>/<job_id>`` | Method: ``DELETE``
Args: Args:
event_id: Identifier of the event event_id: Identifier of the event
@ -381,12 +350,12 @@ def delete_job(event_id, job_id, current_session):
return no_content() return no_content()
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"]) @EventPlugin.blueprint.route("/events/<int:event_id>/<int:job_id>", methods=["PUT"])
@login_required() @login_required(permission=permissions.ASSIGN)
def update_job(event_id, job_id, current_session: Session): def update_job(event_id, job_id, current_session: Session):
"""Edit Job or assign user to the Job """Edit Job or assign user to the Job
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``PUT`` Route: ``/events/<event_id>/<job_id>`` | Method: ``PUT``
POST-data: See TS interface for Job or ``{user: {userid: string, value: number}}`` 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) job = event_controller.get_job(job_id, event_id)
data = request.get_json() data = request.get_json()
if not data: if not data or ("user" not in data and "required_services" not in data):
raise BadRequest raise BadRequest
if ("user" not in data or len(data) > 1) and not current_session.user_.has_permission(permissions.EDIT): # Check if number of services changed, check permission if so
raise Forbidden 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: if "user" in data:
try: try:
user = userController.get_user(data["user"]["userid"]) user = userController.get_user(data["user"]["userid"])
value = data["user"]["value"] value = data["user"].get("value", None)
if (user == current_session.user_ and not user.has_permission(permissions.ASSIGN)) or ( replace = data["user"].get("replace", None)
user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER) if replace is not None:
): replace = userController.get_user(replace)
raise Forbidden event_controller.transfer(job, user, replace)
event_controller.assign_to_job(job, user, value) else:
except (KeyError, ValueError): 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 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() event_controller.update()
return jsonify(job) return jsonify(job)
# TODO: JobTransfer @EventPlugin.blueprint.route("/events/transfer/<int:transfer_id>", 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()