Files
crr-shifts/app/cli.py
T

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