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 ...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
def __init__(self, config):
@ -268,12 +268,20 @@ def create_event(current_session):
except (NotFound, ValueError):
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(
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:
for job in data["jobs"]:
_add_job(event, job)
return jsonify(event)
@ -297,6 +305,8 @@ def modify_event(event_id, current_session):
data = request.get_json()
if "start" in data:
event.start = from_iso_format(data["start"])
if "end" in data:
event.end = from_iso_format(data["end"])
if "description" in data:
event.description = data["description"]
if "type" in data:

View File

@ -3,7 +3,8 @@ from sqlalchemy.exc import IntegrityError
from flaschengeist import logger
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():
@ -97,7 +98,7 @@ def delete_job_type(name):
raise BadRequest("Type still in use")
def get_event(event_id):
def get_event(event_id) -> Event:
event = Event.query.get(event_id)
if event is None:
raise NotFound
@ -128,11 +129,15 @@ def delete_event(event_id):
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:
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)
if recurrence_rule is not None:
event = Event(start=start, end=end, description=description, type=event_type, jobs=jobs, template_=event)
db.session.commit()
return event
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):
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:
raise NotFound
return js
@ -168,7 +173,7 @@ def delete_job(job: Job):
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 not service:
raise BadRequest
@ -180,3 +185,18 @@ def assign_to_job(job: Job, user, value):
service = Service(user_=user, value=value, job_=job)
db.session.add(service)
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",
"roles = flaschengeist.plugins.roles:RolesPlugin",
"balance = flaschengeist.plugins.balance:BalancePlugin",
"schedule = flaschengeist.plugins.schedule:SchedulePlugin",
"events = flaschengeist.plugins.events:EventPlugin",
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
"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)