[events] Can invite, accept and reject invitations
This commit is contained in:
parent
eb04d305ab
commit
b4086108e4
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue