Compare commits

...

6 Commits

7 changed files with 74 additions and 43 deletions

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone
from enum import IntEnum
from typing import Optional, Tuple
from flaschengeist.models import UtcDateTime
from flaschengeist.models.user import User
from werkzeug.exceptions import BadRequest, Conflict, NotFound
from sqlalchemy.exc import IntegrityError
@ -99,7 +100,6 @@ def get_job_types():
def get_job_type(type_id):
job_type = JobType.query.get(type_id)
print(job_type)
if not job_type:
raise NotFound
return job_type
@ -250,9 +250,9 @@ def get_jobs(user, start=None, end=None, limit=None, offset=None, descending=Non
if end is not None:
query = query.filter(end >= Job.start)
if descending is not None:
query = query.order_by(Job.start.desc(), Job.type_id_)
query = query.order_by(Job.start.desc(), Job.type)
else:
query = query.order_by(Job.start, Job.type_id_)
query = query.order_by(Job.start, Job.type)
count = query.count()
if limit is not None:
query = query.limit(limit)
@ -264,7 +264,7 @@ def get_jobs(user, start=None, end=None, limit=None, offset=None, descending=Non
def add_job(event, job_type, required_services, start, end=None, comment=None):
job = Job(
required_services=required_services,
type=job_type,
type_=job_type,
start=start,
end=end,
comment=comment,
@ -340,6 +340,12 @@ def get_invitation(id: int):
return inv
def get_invitations(user: User):
return Invitation.query.filter(
(Invitation.invitee_ == user) | (Invitation.inviter_ == user) | (Invitation.transferee_ == user)
).all()
def cancel_invitation(inv: Invitation):
db.session.delete(inv)
db.session.commit()
@ -386,7 +392,7 @@ def respond_invitation(invite: Invitation, accepted=True):
)
@scheduled(id='dev.flaschengeist.events.assign_backups', minutes=30)
@scheduled(id="dev.flaschengeist.events.assign_backups", minutes=30)
def assign_backups():
now = datetime.now(tz=timezone.utc)
# now + backup_time + next cron tick

View File

@ -62,17 +62,17 @@ class Job(db.Model, ModelSerializeMixin):
id: int = db.Column(Serial, primary_key=True)
start: datetime = db.Column(UtcDateTime, nullable=False)
end: Optional[datetime] = db.Column(UtcDateTime)
type: Union[JobType, int] = db.relationship("JobType")
comment: Optional[str] = db.Column(db.String(256))
type: int = db.Column("type_id", Serial, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
locked: bool = db.Column(db.Boolean(), default=False, nullable=False)
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)
type_: JobType = db.relationship("JobType")
event_ = db.relationship("Event", back_populates="jobs")
event_id_ = db.Column("event_id", Serial, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
type_id_ = db.Column("type_id", Serial, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
invitations_ = db.relationship("Invitation", cascade="all,delete,delete-orphan", back_populates="job_")
@ -112,6 +112,7 @@ class Invitation(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "invitation"
id: int = db.Column(Serial, primary_key=True)
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
# Dummy properties for API export
invitee_id: str = None # User who was invited to take over

View File

@ -203,9 +203,7 @@ def _add_job(event, data):
start = from_iso_format(data["start"])
end = dict_get(data, "end", None, type=from_iso_format)
required_services = data["required_services"]
job_type = data["type"]
if isinstance(job_type, dict):
job_type = data["type"]["id"]
job_type = int(data["type"])
except (KeyError, ValueError):
raise BadRequest("Missing or invalid POST parameter")
@ -239,29 +237,27 @@ def create_event(current_session):
try:
start = from_iso_format(data["start"])
end = dict_get(data, "end", None, type=from_iso_format)
data_type = data["type"]
if isinstance(data_type, dict):
data_type = data["type"]["id"]
event_type = event_controller.get_event_type(data_type)
event_type = event_controller.get_event_type(int(data["type"]))
event = event_controller.create_event(
start=start,
end=end,
name=dict_get(data, "name", None, type=str),
is_template=dict_get(data, "is_template", None, type=bool),
event_type=event_type,
description=dict_get(data, "description", None, type=str),
)
if "jobs" in data:
for job in data["jobs"]:
_add_job(event, job)
return jsonify(event)
except KeyError:
raise BadRequest("Missing POST parameter")
except (NotFound, ValueError):
raise BadRequest("Invalid parameter")
event = event_controller.create_event(
start=start,
end=end,
name=dict_get(data, "name", None),
is_template=dict_get(data, "is_template", None),
event_type=event_type,
description=dict_get(data, "description", None),
)
if "jobs" in data:
for job in data["jobs"]:
_add_job(event, job)
return jsonify(event)
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
@ -378,8 +374,8 @@ def update_job(event_id, job_id, current_session: Session):
job = event_controller.get_job(job_id, event_id)
try:
if "type" in data:
job.type = event_controller.get_job_type(data["type"])
if "type" in data or "type_id" in data:
job.type_ = event_controller.get_job_type(data.get("type", None) or data["type_id"])
job.start = from_iso_format(data.get("start", job.start))
job.end = from_iso_format(data.get("end", job.end))
job.comment = str(data.get("comment", job.comment))
@ -401,6 +397,12 @@ def get_jobs(current_session: Session):
return jsonify({"count": count, "result": result})
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>", methods=["GET"])
@login_required()
def get_job(job_id, current_session: Session):
return jsonify(event_controller.get_job(job_id))
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/assign", methods=["POST"])
@login_required()
def assign_job(job_id, current_session: Session):
@ -492,7 +494,9 @@ def invite(current_session: Session):
raise BadRequest
return jsonify(
[
event_controller.invite(job, invitee, current_session.user_, userController.get_user(transferee) if transferee else None)
event_controller.invite(
job, invitee, current_session.user_, userController.get_user(transferee) if transferee else None
)
for invitee in [userController.get_user(uid) for uid in data["invitees"]]
]
)
@ -500,12 +504,18 @@ def invite(current_session: Session):
raise BadRequest
@EventPlugin.blueprint.route("/events/invitations", methods=["GET"])
@login_required()
def get_invitations(current_session: Session):
return jsonify(event_controller.get_invitations(current_session.user_))
@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
raise NotFound
return jsonify(inv)

View File

@ -20,6 +20,7 @@ classifiers =
universal = True
[options]
python_requires = >=3.8
packages =
flaschengeist_events
install_requires =

2
src/api.d.ts vendored
View File

@ -15,11 +15,11 @@ declare namespace FG {
}
interface Invitation {
id: number;
time: Date;
job_id: number;
invitee_id: string;
inviter_id: string;
transferee_id?: string;
transferee: User;
}
interface Job {
id: number;

View File

@ -22,6 +22,16 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
component: () => import('../pages/EventOverview.vue'),
},
},
{
title: 'Dienstanfragen',
icon: 'mdi-account-switch',
shortcut: false,
route: {
path: 'events-requests',
name: 'events-requests',
component: () => import('../pages/EventRequests.vue'),
},
},
{
title: 'Dienstverwaltung',
icon: 'mdi-account-details',
@ -34,16 +44,6 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
props: (route) => ({ date: route.query.date }),
},
},
{
title: 'Dienstanfragen',
icon: 'mdi-account-switch',
shortcut: false,
route: {
path: 'events-requests',
name: 'events-requests',
component: () => import('../pages/EventRequests.vue'),
},
},
],
},
];

View File

@ -28,6 +28,7 @@ export const useEventStore = defineStore({
jobTypes: [] as FG.JobType[],
eventTypes: [] as FG.EventType[],
templates: [] as FG.Event[],
invitations: [] as FG.Invitation[],
}),
getters: {},
@ -168,6 +169,10 @@ export const useEventStore = defineStore({
.then(({ data }) => fixJob(data));
},
async getJob(id: number) {
return api.get<FG.Job>(`/events/jobs/${id}`).then(({ data }) => fixJob(data));
},
async getJobs(filter?: FG.PaginationFilter) {
return api
.get<FG.PaginationResponse<FG.Job>>('/events/jobs', { params: <unknown>filter })
@ -191,6 +196,14 @@ export const useEventStore = defineStore({
});
},
async getInvitations(force = false) {
if (this.invitations.length == 0 || force) {
const { data } = await api.get<FG.Invitation[]>('/events/invitations');
this.invitations = data;
}
return this.invitations;
},
async rejectInvitation(invite: FG.Invitation | number) {
return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
},