Renamed schedule to events, added recurring events and invites

This commit is contained in:
Ferdinand Thiessen 2021-03-20 17:18:17 +01:00
parent e31c5756a6
commit 4cd353cf4e
7 changed files with 199 additions and 98 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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]"
)

View File

@ -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",
], ],

17
tests/test_events.py Normal file
View File

@ -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)