[events] Can invite, accept and reject invitations

This commit is contained in:
Ferdinand Thiessen 2021-11-24 21:49:14 +01:00
parent eb04d305ab
commit b4086108e4
3 changed files with 150 additions and 15 deletions

View File

@ -1,16 +1,32 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from enum import IntEnum
from typing import Optional from typing import Optional
from werkzeug.exceptions import BadRequest, Conflict, NotFound from werkzeug.exceptions import BadRequest, Conflict, NotFound
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.util import was_deleted
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, Invitation, Job, JobType, Service
from flaschengeist.utils.scheduler import scheduled from flaschengeist.utils.scheduler import scheduled
# STUB
def _(x):
return x
class NotifyType(IntEnum):
# Invitations 0x00..0x0F
INVITE = 0x01
TRANSFER = 0x02
# Invitation responsed 0x10..0x1F
INVITATION_ACCEPTED = 0x10
INVITATION_REJECTED = 0x11
def update(): def update():
db.session.commit() db.session.commit()
@ -232,6 +248,8 @@ def update():
def delete_job(job: Job): def delete_job(job: Job):
for service in job.services: for service in job.services:
unassign_job(service=service, notify=True) unassign_job(service=service, notify=True)
for invitation in job.invitations_:
respond_invitation(invitation, False)
db.session.delete(job) db.session.delete(job)
db.session.commit() db.session.commit()
@ -242,8 +260,7 @@ def assign_job(job: Job, user, value, is_backup=False):
if service: if service:
service.value = value service.value = value
else: else:
service = Service(user_=user, value=value, is_backup=is_backup, job_=job) job.services.append(Service(user_=user, value=value, is_backup=is_backup, job_=job))
db.session.add(service)
db.session.commit() db.session.commit()
@ -264,6 +281,52 @@ def unassign_job(job: Job = None, user=None, service=None, notify=False):
EventPlugin.plugin.notify(user, "Your assignmet was cancelled", {"event_id": event_id}) EventPlugin.plugin.notify(user, "Your assignmet was cancelled", {"event_id": event_id})
def invite(job: Job, invitee, inviter, transferee=None):
inv = Invitation(job_=job, inviter_=inviter, invitee_=invitee, transferee_=transferee)
db.session.add(inv)
update()
if transferee is None:
EventPlugin.plugin.notify(invitee, _("Job invitation"), {"type": NotifyType.INVITE, "invitation": inv.id})
else:
EventPlugin.plugin.notify(invitee, _("Job transfer"), {"type": NotifyType.TRANSFER, "invitation": inv.id})
return inv
def get_invitation(id: int):
inv: Invitation = Invitation.query.get(id)
if inv is None:
raise NotFound
return inv
def cancel_invitation(inv: Invitation):
db.session.delete(inv)
db.session.commit()
def respond_invitation(invite: Invitation, accepted=True):
inviter = invite.inviter_
job = invite.job_
db.session.delete(invite)
db.session.commit()
if not was_deleted(invite):
raise Conflict
if not accepted:
EventPlugin.plugin.notify(inviter, _("Invitation rejected"), {"type": NotifyType.INVITATION_REJECTED, "event": job.event_id_, "job": invite.job_id, "invitee": invite.invitee_id})
else:
if invite.transferee_id is None:
assign_job(job, invite.invitee_, 1)
else:
service = filter(lambda s: s.userid == invite.transferee_id, job.services)
if not service:
raise Conflict
unassign_job(job, invite.transferee_, service[0], True)
assign_job(job, invite.invitee_, service[0].value)
EventPlugin.plugin.notify(inviter, _("Invitation accepted"), {"type": NotifyType.INVITATION_ACCEPTED, "event": job.event_id_, "job": invite.job_id, "invitee": invite.invitee_id})
@scheduled @scheduled
def assign_backups(): def assign_backups():
logger.debug("Notifications") logger.debug("Notifications")

View File

@ -73,6 +73,7 @@ class Job(db.Model, ModelSerializeMixin):
event_ = db.relationship("Event", back_populates="jobs") event_ = db.relationship("Event", back_populates="jobs")
event_id_ = db.Column("event_id", Serial, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False) event_id_ = db.Column("event_id", Serial, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
invitations_ = db.relationship("Invitation", cascade="all,delete,delete-orphan", back_populates="job_")
__table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),) __table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),)
@ -106,25 +107,33 @@ class Event(db.Model, ModelSerializeMixin):
) )
class Invite(db.Model, ModelSerializeMixin): class Invitation(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "invite" __tablename__ = _table_prefix_ + "invitation"
id: int = db.Column(Serial, primary_key=True) id: int = db.Column(Serial, primary_key=True)
job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False) job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
# Dummy properties for API export # Dummy properties for API export
invitee_id: str = None invitee_id: str = None # User who was invited to take over
sender_id: str = None inviter_id: str = None # User who invited the invitee
transferee_id: Optional[str] = None # In case of a transfer: The user who is transfered out of the job
# Not exported properties for backend use # Not exported properties for backend use
invitee_: User = db.relationship("User", foreign_keys="Invite._invitee_id") job_: Job = db.relationship(Job, foreign_keys="Invitation.job_id")
sender_: User = db.relationship("User", foreign_keys="Invite._sender_id") invitee_: User = db.relationship("User", foreign_keys="Invitation._invitee_id")
inviter_: User = db.relationship("User", foreign_keys="Invitation._inviter_id")
transferee_: User = db.relationship("User", foreign_keys="Invitation._transferee_id")
# Protected properties needed for internal use # Protected properties needed for internal use
_invitee_id = db.Column("invitee_id", Serial, db.ForeignKey("user.id"), nullable=False) _invitee_id = db.Column("invitee_id", Serial, db.ForeignKey("user.id"), nullable=False)
_sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id"), nullable=False) _inviter_id = db.Column("inviter_id", Serial, db.ForeignKey("user.id"), nullable=False)
_transferee_id = db.Column("transferee_id", Serial, db.ForeignKey("user.id"))
@property @property
def invitee_id(self): def invitee_id(self):
return self.invitee_.userid return self.invitee_.userid
@property @property
def sender_id(self): def inviter_id(self):
return self.sender_.userid return self.inviter_.userid
@property
def transferee_id(self):
return self.transferee_.userid if self.transferee_ else None

View File

@ -463,7 +463,7 @@ def assign_job(job_id, current_session: Session):
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
Returns: Returns:
HTTP-No-Content or HTTP-error JSON encoded Job or HTTP-error
""" """
data = request.get_json() data = request.get_json()
job = event_controller.get_job(job_id) job = event_controller.get_job(job_id)
@ -480,7 +480,7 @@ def assign_job(job_id, current_session: Session):
event_controller.unassign_job(job, user, notify=user != current_session.user_) event_controller.unassign_job(job, user, notify=user != current_session.user_)
except (TypeError, KeyError, ValueError): except (TypeError, KeyError, ValueError):
raise BadRequest raise BadRequest
return no_content() return jsonify(job)
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/lock", methods=["POST"]) @EventPlugin.blueprint.route("/events/jobs/<int:job_id>/lock", methods=["POST"])
@ -510,4 +510,67 @@ def lock_job(job_id, current_session: Session):
return no_content() return no_content()
# TODO: JobTransfer @EventPlugin.blueprint.route("/events/invitations", methods=["POST"])
@login_required()
def invite(current_session: Session):
"""Invite an user to a job or transfer job
Route: ``/events/invites`` | Method: ``POST``
POST-data: ``{job: number, invitees: string[], is_transfer?: boolean}``
Args:
current_session: Session sent with Authorization Header
Returns:
List of Invitation objects or HTTP-error
"""
data = request.get_json()
transferee = data.get("transferee", None)
if (
transferee is not None
and transferee != current_session.userid
and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
):
raise Forbidden
try:
job = event_controller.get_job(data["job"])
if not isinstance(data["invitees"], list):
raise BadRequest
return jsonify(
[
event_controller.invite(job, invitee, current_session.user_, transferee)
for invitee in [userController.get_user(uid) for uid in data["invitees"]]
]
)
except (TypeError, KeyError, ValueError):
raise BadRequest
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["GET"])
@login_required()
def get_invitation(invitation_id: int, current_session: Session):
inv = event_controller.get_invitation(invitation_id)
if current_session.userid not in [inv.invitee_id, inv.inviter_id, inv.transferee_id]:
raise Forbidden
return jsonify(inv)
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["DELETE", "PUT"])
@login_required()
def respond_invitation(invitation_id: int, current_session: Session):
inv = event_controller.get_invitation(invitation_id)
if request.method == "DELETE":
if current_session.userid == inv.invitee_id:
event_controller.respond_invitation(inv, False)
elif current_session.userid == inv.inviter_id:
event_controller.cancel_invitation(inv)
else:
raise Forbidden
else:
# maybe validate data is something like ({accepted: true})
if current_session.userid != inv.invitee_id:
raise Forbidden
event_controller.respond_invitation(inv)
return no_content()