Compare commits
6 Commits
62599898d0
...
166073fb55
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | 166073fb55 | |
Ferdinand Thiessen | 0f65ae53af | |
Ferdinand Thiessen | 2ef9fd023a | |
Ferdinand Thiessen | a09ce26474 | |
Ferdinand Thiessen | 5a52c364e4 | |
Ferdinand Thiessen | 72cb163a00 |
|
@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from flaschengeist.models import UtcDateTime
|
from flaschengeist.models import UtcDateTime
|
||||||
|
from flaschengeist.models.user import User
|
||||||
|
|
||||||
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
@ -99,7 +100,6 @@ def get_job_types():
|
||||||
|
|
||||||
def get_job_type(type_id):
|
def get_job_type(type_id):
|
||||||
job_type = JobType.query.get(type_id)
|
job_type = JobType.query.get(type_id)
|
||||||
print(job_type)
|
|
||||||
if not job_type:
|
if not job_type:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
return job_type
|
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:
|
if end is not None:
|
||||||
query = query.filter(end >= Job.start)
|
query = query.filter(end >= Job.start)
|
||||||
if descending is not None:
|
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:
|
else:
|
||||||
query = query.order_by(Job.start, Job.type_id_)
|
query = query.order_by(Job.start, Job.type)
|
||||||
count = query.count()
|
count = query.count()
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
query = query.limit(limit)
|
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):
|
def add_job(event, job_type, required_services, start, end=None, comment=None):
|
||||||
job = Job(
|
job = Job(
|
||||||
required_services=required_services,
|
required_services=required_services,
|
||||||
type=job_type,
|
type_=job_type,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
|
@ -340,6 +340,12 @@ def get_invitation(id: int):
|
||||||
return inv
|
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):
|
def cancel_invitation(inv: Invitation):
|
||||||
db.session.delete(inv)
|
db.session.delete(inv)
|
||||||
db.session.commit()
|
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():
|
def assign_backups():
|
||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=timezone.utc)
|
||||||
# now + backup_time + next cron tick
|
# now + backup_time + next cron tick
|
||||||
|
|
|
@ -62,17 +62,17 @@ class Job(db.Model, ModelSerializeMixin):
|
||||||
id: int = db.Column(Serial, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
start: datetime = db.Column(UtcDateTime, nullable=False)
|
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||||
end: Optional[datetime] = db.Column(UtcDateTime)
|
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
type: Union[JobType, int] = db.relationship("JobType")
|
|
||||||
comment: Optional[str] = db.Column(db.String(256))
|
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)
|
locked: bool = db.Column(db.Boolean(), default=False, nullable=False)
|
||||||
services: list[Service] = db.relationship(
|
services: list[Service] = db.relationship(
|
||||||
"Service", back_populates="job_", cascade="save-update, merge, delete, delete-orphan"
|
"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)
|
||||||
|
|
||||||
|
type_: JobType = db.relationship("JobType")
|
||||||
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)
|
||||||
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_")
|
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"
|
__tablename__ = _table_prefix_ + "invitation"
|
||||||
|
|
||||||
id: int = db.Column(Serial, primary_key=True)
|
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)
|
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 # User who was invited to take over
|
invitee_id: str = None # User who was invited to take over
|
||||||
|
|
|
@ -203,9 +203,7 @@ def _add_job(event, data):
|
||||||
start = from_iso_format(data["start"])
|
start = from_iso_format(data["start"])
|
||||||
end = dict_get(data, "end", None, type=from_iso_format)
|
end = dict_get(data, "end", None, type=from_iso_format)
|
||||||
required_services = data["required_services"]
|
required_services = data["required_services"]
|
||||||
job_type = data["type"]
|
job_type = int(data["type"])
|
||||||
if isinstance(job_type, dict):
|
|
||||||
job_type = data["type"]["id"]
|
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
raise BadRequest("Missing or invalid POST parameter")
|
raise BadRequest("Missing or invalid POST parameter")
|
||||||
|
|
||||||
|
@ -239,29 +237,27 @@ def create_event(current_session):
|
||||||
try:
|
try:
|
||||||
start = from_iso_format(data["start"])
|
start = from_iso_format(data["start"])
|
||||||
end = dict_get(data, "end", None, type=from_iso_format)
|
end = dict_get(data, "end", None, type=from_iso_format)
|
||||||
data_type = data["type"]
|
event_type = event_controller.get_event_type(int(data["type"]))
|
||||||
if isinstance(data_type, dict):
|
|
||||||
data_type = data["type"]["id"]
|
event = event_controller.create_event(
|
||||||
event_type = event_controller.get_event_type(data_type)
|
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:
|
except KeyError:
|
||||||
raise BadRequest("Missing POST parameter")
|
raise BadRequest("Missing POST parameter")
|
||||||
except (NotFound, ValueError):
|
except (NotFound, ValueError):
|
||||||
raise BadRequest("Invalid parameter")
|
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"])
|
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["PUT"])
|
||||||
@login_required(permission=permissions.EDIT)
|
@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)
|
job = event_controller.get_job(job_id, event_id)
|
||||||
try:
|
try:
|
||||||
if "type" in data:
|
if "type" in data or "type_id" in data:
|
||||||
job.type = event_controller.get_job_type(data["type"])
|
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.start = from_iso_format(data.get("start", job.start))
|
||||||
job.end = from_iso_format(data.get("end", job.end))
|
job.end = from_iso_format(data.get("end", job.end))
|
||||||
job.comment = str(data.get("comment", job.comment))
|
job.comment = str(data.get("comment", job.comment))
|
||||||
|
@ -401,6 +397,12 @@ def get_jobs(current_session: Session):
|
||||||
return jsonify({"count": count, "result": result})
|
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"])
|
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/assign", methods=["POST"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def assign_job(job_id, current_session: Session):
|
def assign_job(job_id, current_session: Session):
|
||||||
|
@ -492,7 +494,9 @@ def invite(current_session: Session):
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
return jsonify(
|
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"]]
|
for invitee in [userController.get_user(uid) for uid in data["invitees"]]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -500,12 +504,18 @@ def invite(current_session: Session):
|
||||||
raise BadRequest
|
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"])
|
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_invitation(invitation_id: int, current_session: Session):
|
def get_invitation(invitation_id: int, current_session: Session):
|
||||||
inv = event_controller.get_invitation(invitation_id)
|
inv = event_controller.get_invitation(invitation_id)
|
||||||
if current_session.userid not in [inv.invitee_id, inv.inviter_id, inv.transferee_id]:
|
if current_session.userid not in [inv.invitee_id, inv.inviter_id, inv.transferee_id]:
|
||||||
raise Forbidden
|
raise NotFound
|
||||||
return jsonify(inv)
|
return jsonify(inv)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ classifiers =
|
||||||
universal = True
|
universal = True
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
python_requires = >=3.8
|
||||||
packages =
|
packages =
|
||||||
flaschengeist_events
|
flaschengeist_events
|
||||||
install_requires =
|
install_requires =
|
||||||
|
|
|
@ -15,11 +15,11 @@ declare namespace FG {
|
||||||
}
|
}
|
||||||
interface Invitation {
|
interface Invitation {
|
||||||
id: number;
|
id: number;
|
||||||
|
time: Date;
|
||||||
job_id: number;
|
job_id: number;
|
||||||
invitee_id: string;
|
invitee_id: string;
|
||||||
inviter_id: string;
|
inviter_id: string;
|
||||||
transferee_id?: string;
|
transferee_id?: string;
|
||||||
transferee: User;
|
|
||||||
}
|
}
|
||||||
interface Job {
|
interface Job {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
@ -22,6 +22,16 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
component: () => import('../pages/EventOverview.vue'),
|
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',
|
title: 'Dienstverwaltung',
|
||||||
icon: 'mdi-account-details',
|
icon: 'mdi-account-details',
|
||||||
|
@ -34,16 +44,6 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
props: (route) => ({ date: route.query.date }),
|
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'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const useEventStore = defineStore({
|
||||||
jobTypes: [] as FG.JobType[],
|
jobTypes: [] as FG.JobType[],
|
||||||
eventTypes: [] as FG.EventType[],
|
eventTypes: [] as FG.EventType[],
|
||||||
templates: [] as FG.Event[],
|
templates: [] as FG.Event[],
|
||||||
|
invitations: [] as FG.Invitation[],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {},
|
getters: {},
|
||||||
|
@ -168,6 +169,10 @@ export const useEventStore = defineStore({
|
||||||
.then(({ data }) => fixJob(data));
|
.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) {
|
async getJobs(filter?: FG.PaginationFilter) {
|
||||||
return api
|
return api
|
||||||
.get<FG.PaginationResponse<FG.Job>>('/events/jobs', { params: <unknown>filter })
|
.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) {
|
async rejectInvitation(invite: FG.Invitation | number) {
|
||||||
return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
|
return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue