Renamed schedule to events, added recurring events and invites
This commit is contained in:
parent
e31c5756a6
commit
4cd353cf4e
|
@ -17,10 +17,10 @@ from . import event_controller, permissions
|
||||||
from . import models
|
from . import models
|
||||||
from ...utils.HTTP import no_content
|
from ...utils.HTTP import no_content
|
||||||
|
|
||||||
schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule")
|
schedule_bp = Blueprint("events", __name__, url_prefix="/schedule")
|
||||||
|
|
||||||
|
|
||||||
class SchedulePlugin(Plugin):
|
class EventPlugin(Plugin):
|
||||||
models = models
|
models = models
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
|
@ -268,12 +268,20 @@ def create_event(current_session):
|
||||||
except (NotFound, ValueError):
|
except (NotFound, ValueError):
|
||||||
raise BadRequest("Invalid parameter")
|
raise BadRequest("Invalid parameter")
|
||||||
|
|
||||||
|
recurrence_rule = None
|
||||||
|
if "recurrence_rule" in data:
|
||||||
|
recurrence_rule = event_controller.create_recurrence(data=data["recurrence_rule"])
|
||||||
event = event_controller.create_event(
|
event = event_controller.create_event(
|
||||||
start=start, end=end, event_type=event_type, description=data.get("description", None)
|
start=start,
|
||||||
|
end=end,
|
||||||
|
event_type=event_type,
|
||||||
|
description=data.get("description", None),
|
||||||
|
recurrence_rule=recurrence_rule,
|
||||||
)
|
)
|
||||||
if "jobs" in data:
|
if "jobs" in data:
|
||||||
for job in data["jobs"]:
|
for job in data["jobs"]:
|
||||||
_add_job(event, job)
|
_add_job(event, job)
|
||||||
|
|
||||||
return jsonify(event)
|
return jsonify(event)
|
||||||
|
|
||||||
|
|
||||||
|
@ -297,6 +305,8 @@ def modify_event(event_id, current_session):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if "start" in data:
|
if "start" in data:
|
||||||
event.start = from_iso_format(data["start"])
|
event.start = from_iso_format(data["start"])
|
||||||
|
if "end" in data:
|
||||||
|
event.end = from_iso_format(data["end"])
|
||||||
if "description" in data:
|
if "description" in data:
|
||||||
event.description = data["description"]
|
event.description = data["description"]
|
||||||
if "type" in data:
|
if "type" in data:
|
|
@ -3,7 +3,8 @@ from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from flaschengeist.plugins.schedule.models import EventType, Event, Job, JobType, Service
|
from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, Service, RecurrenceRule
|
||||||
|
from flaschengeist.utils.datetime import from_iso_format
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
|
@ -97,7 +98,7 @@ def delete_job_type(name):
|
||||||
raise BadRequest("Type still in use")
|
raise BadRequest("Type still in use")
|
||||||
|
|
||||||
|
|
||||||
def get_event(event_id):
|
def get_event(event_id) -> Event:
|
||||||
event = Event.query.get(event_id)
|
event = Event.query.get(event_id)
|
||||||
if event is None:
|
if event is None:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
|
@ -128,11 +129,15 @@ def delete_event(event_id):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def create_event(event_type, start, end=None, jobs=[], description=None):
|
def create_event(event_type, start, end=None, jobs=[], description=None, recurrence_rule=None):
|
||||||
try:
|
try:
|
||||||
logger.debug(event_type)
|
logger.debug(event_type)
|
||||||
event = Event(start=start, end=end, description=description, type=event_type, jobs=jobs)
|
event = Event(
|
||||||
|
start=start, end=end, description=description, type=event_type, jobs=jobs, recurrence_rule=recurrence_rule
|
||||||
|
)
|
||||||
db.session.add(event)
|
db.session.add(event)
|
||||||
|
if recurrence_rule is not None:
|
||||||
|
event = Event(start=start, end=end, description=description, type=event_type, jobs=jobs, template_=event)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return event
|
return event
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
@ -141,7 +146,7 @@ def create_event(event_type, start, end=None, jobs=[], description=None):
|
||||||
|
|
||||||
|
|
||||||
def get_job(job_slot_id, event_id):
|
def get_job(job_slot_id, event_id):
|
||||||
js = Job.query.filter(Job.id == job_slot_id).filter(Job._event_id == event_id).one_or_none()
|
js = Job.query.filter(Job.id == job_slot_id).filter(Job.event_id_ == event_id).one_or_none()
|
||||||
if js is None:
|
if js is None:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
return js
|
return js
|
||||||
|
@ -168,7 +173,7 @@ def delete_job(job: Job):
|
||||||
|
|
||||||
|
|
||||||
def assign_to_job(job: Job, user, value):
|
def assign_to_job(job: Job, user, value):
|
||||||
service = Service.query.get((job.id, user._id))
|
service = Service.query.get((job.id, user.id_))
|
||||||
if value < 0:
|
if value < 0:
|
||||||
if not service:
|
if not service:
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
|
@ -180,3 +185,18 @@ def assign_to_job(job: Job, user, value):
|
||||||
service = Service(user_=user, value=value, job_=job)
|
service = Service(user_=user, value=value, job_=job)
|
||||||
db.session.add(service)
|
db.session.add(service)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def create_recurrence(data=None, count=None, end_date=None, frequency=None, interval=None):
|
||||||
|
if data is not None:
|
||||||
|
if "frequency" not in data:
|
||||||
|
raise BadRequest("Missing POST parameter")
|
||||||
|
frequency = data["frequency"]
|
||||||
|
if "end_date" in data:
|
||||||
|
end_date = from_iso_format(data["end_date"])
|
||||||
|
if "count" in data:
|
||||||
|
count = data["count"]
|
||||||
|
if "interval" in data:
|
||||||
|
interval = data["interval"]
|
||||||
|
recurrence = RecurrenceRule(frequency=frequency, end_date=end_date, count=count, interval=interval)
|
||||||
|
db.session.add(recurrence)
|
|
@ -0,0 +1,142 @@
|
||||||
|
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
||||||
|
|
||||||
|
import enum
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
|
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
|
||||||
|
from flaschengeist.models.user import User
|
||||||
|
from flaschengeist.database import db
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Types #
|
||||||
|
#########
|
||||||
|
|
||||||
|
_table_prefix_ = "events_"
|
||||||
|
|
||||||
|
|
||||||
|
class EventType(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "event_type"
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class JobType(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "job_type"
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
########
|
||||||
|
# Jobs #
|
||||||
|
########
|
||||||
|
|
||||||
|
|
||||||
|
class Service(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "service"
|
||||||
|
userid: str = ""
|
||||||
|
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
|
_job_id = db.Column(
|
||||||
|
"job_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False, primary_key=True
|
||||||
|
)
|
||||||
|
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True)
|
||||||
|
|
||||||
|
user_: User = db.relationship("User")
|
||||||
|
job_: Job = db.relationship("Job")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def userid(self):
|
||||||
|
return self.user_.userid
|
||||||
|
|
||||||
|
|
||||||
|
class Job(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "job"
|
||||||
|
_type_id = db.Column("type_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
|
||||||
|
|
||||||
|
id: int = db.Column(db.Integer, 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))
|
||||||
|
services: list[Service] = db.relationship("Service", back_populates="job_")
|
||||||
|
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
|
event_ = db.relationship("Event", back_populates="jobs")
|
||||||
|
event_id_ = db.Column("event_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
|
||||||
|
|
||||||
|
__table_args__ = (UniqueConstraint("type_id", "start", name="_type_start_uc"),)
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Events #
|
||||||
|
##########
|
||||||
|
|
||||||
|
|
||||||
|
class _Frequency(enum.Enum):
|
||||||
|
daily = 1
|
||||||
|
weekly = 2
|
||||||
|
monthly = 3
|
||||||
|
yearly = 4
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrenceRule(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "recurrence_rule"
|
||||||
|
|
||||||
|
frequency: str = db.Column(db.Enum(_Frequency))
|
||||||
|
until: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
|
count: Optional[int] = db.Column(db.Integer)
|
||||||
|
interval: int = db.Column(db.Integer, nullable=False, default=1)
|
||||||
|
|
||||||
|
id_: int = db.Column("id", db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Event(db.Model, ModelSerializeMixin):
|
||||||
|
"""Model for an Event"""
|
||||||
|
|
||||||
|
__tablename__ = _table_prefix_ + "event"
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||||
|
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
|
description: Optional[str] = db.Column(db.String(255))
|
||||||
|
type: Union[EventType, int] = db.relationship("EventType")
|
||||||
|
jobs: list[Job] = db.relationship(
|
||||||
|
"Job", back_populates="event_", cascade="all,delete,delete-orphan", order_by="[Job.start, Job.end]"
|
||||||
|
)
|
||||||
|
recurrence_rule: Optional[RecurrenceRule] = db.relationship("RecurrenceRule")
|
||||||
|
template_id: Optional[int] = db.Column("template_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event.id"))
|
||||||
|
# Not exported properties for backend use
|
||||||
|
template_ = db.relationship("Event")
|
||||||
|
# Protected for internal use
|
||||||
|
_recurrence_rule_id = db.Column(
|
||||||
|
"recurrence_rule_id", db.Integer, db.ForeignKey(f"{_table_prefix_}recurrence_rule.id")
|
||||||
|
)
|
||||||
|
_type_id = db.Column(
|
||||||
|
"type_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event_type.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Invite(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = _table_prefix_ + "invite"
|
||||||
|
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
job_id: int = db.Column(db.Integer, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
|
||||||
|
# Dummy properties for API export
|
||||||
|
invitee_id: str = None
|
||||||
|
sender_id: str = None
|
||||||
|
# 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")
|
||||||
|
# Protected properties needed for internal use
|
||||||
|
_invitee_id = db.Column("invitee_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def invitee_id(self):
|
||||||
|
return self.invitee_.userid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sender_id(self):
|
||||||
|
return self.sender_.userid
|
|
@ -1,88 +0,0 @@
|
||||||
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
|
||||||
|
|
||||||
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
|
|
||||||
from flaschengeist.models.user import User
|
|
||||||
from flaschengeist.database import db
|
|
||||||
|
|
||||||
#########
|
|
||||||
# Types #
|
|
||||||
#########
|
|
||||||
|
|
||||||
|
|
||||||
class EventType(db.Model, ModelSerializeMixin):
|
|
||||||
__tablename__ = "schedule_event_type"
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
|
||||||
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
|
||||||
|
|
||||||
|
|
||||||
class JobType(db.Model, ModelSerializeMixin):
|
|
||||||
__tablename__ = "schedule_job_type"
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
|
||||||
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
|
||||||
|
|
||||||
|
|
||||||
########
|
|
||||||
# Jobs #
|
|
||||||
########
|
|
||||||
|
|
||||||
|
|
||||||
class Service(db.Model, ModelSerializeMixin):
|
|
||||||
__tablename__ = "schedule_service"
|
|
||||||
userid: str = ""
|
|
||||||
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
|
|
||||||
|
|
||||||
_job_id = db.Column("job_id", db.Integer, db.ForeignKey("schedule_job.id"), nullable=False, primary_key=True)
|
|
||||||
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True)
|
|
||||||
|
|
||||||
user_: User = db.relationship("User")
|
|
||||||
job_ = db.relationship("Job")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def userid(self):
|
|
||||||
return self.user_.userid
|
|
||||||
|
|
||||||
|
|
||||||
class Job(db.Model, ModelSerializeMixin):
|
|
||||||
__tablename__ = "schedule_job"
|
|
||||||
_type_id = db.Column("type_id", db.Integer, db.ForeignKey("schedule_job_type.id"), nullable=False)
|
|
||||||
_event_id = db.Column("event_id", db.Integer, db.ForeignKey("schedule_event.id"), nullable=False)
|
|
||||||
|
|
||||||
id: int = db.Column(db.Integer, 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))
|
|
||||||
services: list[Service] = db.relationship("Service", back_populates="job_")
|
|
||||||
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
|
|
||||||
|
|
||||||
event_ = db.relationship("Event", back_populates="jobs")
|
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint("type_id", "start", name="_type_start_uc"),)
|
|
||||||
|
|
||||||
|
|
||||||
##########
|
|
||||||
# Events #
|
|
||||||
##########
|
|
||||||
|
|
||||||
|
|
||||||
class Event(db.Model, ModelSerializeMixin):
|
|
||||||
"""Model for an Event"""
|
|
||||||
|
|
||||||
__tablename__ = "schedule_event"
|
|
||||||
_type_id = db.Column(
|
|
||||||
"type_id", db.Integer, db.ForeignKey("schedule_event_type.id", ondelete="CASCADE"), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
|
||||||
start: datetime = db.Column(UtcDateTime, nullable=False)
|
|
||||||
end: Optional[datetime] = db.Column(UtcDateTime)
|
|
||||||
description: Optional[str] = db.Column(db.String(255))
|
|
||||||
type: Union[EventType, int] = db.relationship("EventType")
|
|
||||||
jobs: list[Job] = db.relationship(
|
|
||||||
"Job", back_populates="event_", cascade="all,delete,delete-orphan", order_by="[Job.start, Job.end]"
|
|
||||||
)
|
|
2
setup.py
2
setup.py
|
@ -34,7 +34,7 @@ setup(
|
||||||
"users = flaschengeist.plugins.users:UsersPlugin",
|
"users = flaschengeist.plugins.users:UsersPlugin",
|
||||||
"roles = flaschengeist.plugins.roles:RolesPlugin",
|
"roles = flaschengeist.plugins.roles:RolesPlugin",
|
||||||
"balance = flaschengeist.plugins.balance:BalancePlugin",
|
"balance = flaschengeist.plugins.balance:BalancePlugin",
|
||||||
"schedule = flaschengeist.plugins.schedule:SchedulePlugin",
|
"events = flaschengeist.plugins.events:EventPlugin",
|
||||||
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
|
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
|
||||||
"pricelist = flaschengeist.plugins.pricelist:PriceListPlugin",
|
"pricelist = flaschengeist.plugins.pricelist:PriceListPlugin",
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import pytest
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
|
||||||
|
import flaschengeist.plugins.events.event_controller as event_controller
|
||||||
|
from flaschengeist.plugins.events.models import EventType
|
||||||
|
|
||||||
|
VALID_TOKEN = "f4ecbe14be3527ca998143a49200e294"
|
||||||
|
EVENT_TYPE_NAME = "Test Type"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_event_type(app):
|
||||||
|
with app.app_context():
|
||||||
|
type = event_controller.create_event_type(EVENT_TYPE_NAME)
|
||||||
|
assert isinstance(type, EventType)
|
||||||
|
|
||||||
|
with pytest.raises(BadRequest):
|
||||||
|
event_controller.create_event_type(EVENT_TYPE_NAME)
|
Loading…
Reference in New Issue