167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import code
|
|
import sys
|
|
from datetime import date, timedelta
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from .bootstrap import ensure_initial_data
|
|
from .config import settings
|
|
from .database import (
|
|
SessionLocal,
|
|
current_revision,
|
|
has_tables,
|
|
run_migrations,
|
|
stamp,
|
|
)
|
|
from . import models
|
|
from .models import Assignment, MonthlyPattern, NotificationLog, RotationMember, Schedule, User
|
|
from .notifications import send_email_notification
|
|
from .scheduling import ensure_month_assignments
|
|
from .security import hash_password
|
|
|
|
BASELINE_REVISION = "0001_initial"
|
|
|
|
|
|
def migrate() -> None:
|
|
if has_tables() and current_revision() is None:
|
|
stamp(BASELINE_REVISION)
|
|
print(f"Existing database stamped at {BASELINE_REVISION}.")
|
|
run_migrations()
|
|
print(f"Database at revision {current_revision()}.")
|
|
|
|
|
|
def init_db() -> None:
|
|
migrate()
|
|
with SessionLocal() as db:
|
|
ensure_initial_data(db)
|
|
print("Database initialized.")
|
|
|
|
|
|
def seed_demo() -> None:
|
|
init_db()
|
|
demo_people = [
|
|
("Alice", "alice", "alice@example.com"),
|
|
("Bruno", "bruno", "bruno@example.com"),
|
|
("Carla", "carla", None),
|
|
("Diogo", "diogo", "diogo@example.com"),
|
|
]
|
|
with SessionLocal() as db:
|
|
for name, username, email in demo_people:
|
|
if not db.scalar(select(User.id).where(User.username == username)):
|
|
db.add(
|
|
User(
|
|
name=name,
|
|
username=username,
|
|
email=email,
|
|
phone=None,
|
|
password_hash=hash_password("demo-password"),
|
|
is_active=True,
|
|
is_admin=False,
|
|
notify_email=True,
|
|
)
|
|
)
|
|
db.commit()
|
|
people = db.scalars(select(User).where(User.username.in_([x[1] for x in demo_people])).order_by(User.name)).all()
|
|
schedules = db.scalars(
|
|
select(Schedule).where(Schedule.slug.in_(["saturday-lunch", "sunday-lunch"])).order_by(Schedule.id)
|
|
).all()
|
|
for schedule in schedules:
|
|
if not db.scalar(select(RotationMember.id).where(RotationMember.schedule_id == schedule.id)):
|
|
for pos, person in enumerate(people, start=1):
|
|
db.add(RotationMember(schedule_id=schedule.id, user_id=person.id, position=pos))
|
|
|
|
night = db.scalar(select(Schedule).where(Schedule.slug == "night"))
|
|
if night is not None and not db.scalar(
|
|
select(MonthlyPattern.id).where(MonthlyPattern.schedule_id == night.id)
|
|
):
|
|
for day in range(1, 32):
|
|
db.add(MonthlyPattern(
|
|
schedule_id=night.id,
|
|
day_of_month=day,
|
|
user_id=people[(day - 1) % len(people)].id,
|
|
))
|
|
db.commit()
|
|
|
|
today = date.today()
|
|
next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
|
|
for month in (today, next_month):
|
|
ensure_month_assignments(db, month.year, month.month)
|
|
print("Demo data seeded for this month and next. Existing assignments and configuration preserved.")
|
|
print("Demo logins: alice, bruno, carla, diogo. Password: demo-password (new accounts only).")
|
|
|
|
|
|
def db_shell() -> None:
|
|
"""An interactive SQLAlchemy session against the configured application database."""
|
|
init_db()
|
|
with SessionLocal() as db:
|
|
namespace = {"db": db, "select": select, "models": models}
|
|
namespace.update({name: getattr(models, name) for name in (
|
|
"User", "Schedule", "MonthlyPattern", "RotationMember", "Assignment", "SwapRequest", "NotificationLog"
|
|
)})
|
|
code.interact(
|
|
banner="Database console: db, select, models and model classes are available.\n"
|
|
"Example: db.scalars(select(User)).all()\n"
|
|
"Use db.commit() to save edits, db.rollback() to discard them; Ctrl-D to exit.",
|
|
local=namespace,
|
|
)
|
|
|
|
|
|
def send_reminders() -> None:
|
|
migrate()
|
|
target = date.today() + timedelta(days=settings.reminder_days_ahead)
|
|
with SessionLocal() as db:
|
|
ensure_initial_data(db)
|
|
ensure_month_assignments(db, target.year, target.month)
|
|
assignments = db.scalars(
|
|
select(Assignment)
|
|
.where(Assignment.date == target, Assignment.user_id.is_not(None))
|
|
.options(selectinload(Assignment.user), selectinload(Assignment.schedule))
|
|
).all()
|
|
count = 0
|
|
for assignment in assignments:
|
|
if not assignment.user:
|
|
continue
|
|
subject = f"Rota reminder: {assignment.schedule.name} on {assignment.date:%d %b}"
|
|
already_sent = db.scalar(
|
|
select(NotificationLog.id).where(
|
|
NotificationLog.user_id == assignment.user.id,
|
|
NotificationLog.event_type == "shift_reminder",
|
|
NotificationLog.subject == subject,
|
|
NotificationLog.status == "sent",
|
|
)
|
|
)
|
|
if already_sent:
|
|
continue
|
|
send_email_notification(
|
|
db,
|
|
assignment.user,
|
|
"shift_reminder",
|
|
subject,
|
|
f"Reminder: you are assigned to {assignment.schedule.name} on "
|
|
f"{assignment.date:%A, %d %B %Y}.\n\nView the rota: {settings.normalized_base_url}/",
|
|
)
|
|
count += 1
|
|
print(f"Processed {count} reminder(s) for {target.isoformat()}.")
|
|
|
|
|
|
def main() -> None:
|
|
command = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
commands = {
|
|
"init-db": init_db,
|
|
"migrate": migrate,
|
|
"seed-demo": seed_demo,
|
|
"db-shell": db_shell,
|
|
"send-reminders": send_reminders,
|
|
}
|
|
if command not in commands:
|
|
print("Usage: python -m app.cli [init-db|migrate|seed-demo|db-shell|send-reminders]")
|
|
raise SystemExit(2)
|
|
commands[command]()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|