Merge remote-tracking branch 'origin/develop' into feature/pricelist
This commit is contained in:
		
						commit
						11b7f05ad7
					
				|  | @ -1,16 +1,32 @@ | |||
| from datetime import datetime, timedelta, timezone | ||||
| from enum import IntEnum | ||||
| from typing import Optional | ||||
| 
 | ||||
| from werkzeug.exceptions import BadRequest, Conflict, NotFound | ||||
| from sqlalchemy.exc import IntegrityError | ||||
| from sqlalchemy.orm.util import was_deleted | ||||
| 
 | ||||
| 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, Invitation, Job, JobType, Service | ||||
| 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(): | ||||
|     db.session.commit() | ||||
| 
 | ||||
|  | @ -232,6 +248,8 @@ def update(): | |||
| def delete_job(job: Job): | ||||
|     for service in job.services: | ||||
|         unassign_job(service=service, notify=True) | ||||
|     for invitation in job.invitations_: | ||||
|         respond_invitation(invitation, False) | ||||
|     db.session.delete(job) | ||||
|     db.session.commit() | ||||
| 
 | ||||
|  | @ -242,8 +260,7 @@ def assign_job(job: Job, user, value, is_backup=False): | |||
|     if service: | ||||
|         service.value = value | ||||
|     else: | ||||
|         service = Service(user_=user, value=value, is_backup=is_backup, job_=job) | ||||
|         db.session.add(service) | ||||
|         job.services.append(Service(user_=user, value=value, is_backup=is_backup, job_=job)) | ||||
|     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}) | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
| def assign_backups(): | ||||
|     logger.debug("Notifications") | ||||
|  |  | |||
|  | @ -73,6 +73,7 @@ class Job(db.Model, ModelSerializeMixin): | |||
| 
 | ||||
|     event_ = db.relationship("Event", back_populates="jobs") | ||||
|     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"),) | ||||
| 
 | ||||
|  | @ -106,25 +107,33 @@ class Event(db.Model, ModelSerializeMixin): | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class Invite(db.Model, ModelSerializeMixin): | ||||
|     __tablename__ = _table_prefix_ + "invite" | ||||
| class Invitation(db.Model, ModelSerializeMixin): | ||||
|     __tablename__ = _table_prefix_ + "invitation" | ||||
| 
 | ||||
|     id: int = db.Column(Serial, primary_key=True) | ||||
|     job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False) | ||||
|     # Dummy properties for API export | ||||
|     invitee_id: str = None | ||||
|     sender_id: str = None | ||||
|     invitee_id: str = None  # User who was invited to take over | ||||
|     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 | ||||
|     invitee_: User = db.relationship("User", foreign_keys="Invite._invitee_id") | ||||
|     sender_: User = db.relationship("User", foreign_keys="Invite._sender_id") | ||||
|     job_: Job = db.relationship(Job, foreign_keys="Invitation.job_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 | ||||
|     _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 | ||||
|     def invitee_id(self): | ||||
|         return self.invitee_.userid | ||||
| 
 | ||||
|     @property | ||||
|     def sender_id(self): | ||||
|         return self.sender_.userid | ||||
|     def inviter_id(self): | ||||
|         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 | ||||
| 
 | ||||
|     Returns: | ||||
|         HTTP-No-Content or HTTP-error | ||||
|         JSON encoded Job or HTTP-error | ||||
|     """ | ||||
|     data = request.get_json() | ||||
|     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_) | ||||
|     except (TypeError, KeyError, ValueError): | ||||
|         raise BadRequest | ||||
|     return no_content() | ||||
|     return jsonify(job) | ||||
| 
 | ||||
| 
 | ||||
| @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() | ||||
| 
 | ||||
| 
 | ||||
| # 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