Files
LinkDesk/backend/generate_schedule_example_data.py
indigo f547d05478 Add project Schedule page with interactive Gantt chart
Adds a Schedule tab (Kitsu-style production schedule) under each
project: tasks grouped by type with drag-to-reschedule bars, Day/Week/
Month zoom, manual date-range control, weekend shading, a frozen task
column, and a two-tier month/date axis header. Requires a new
start_date field on Task (start_date was previously missing; only
deadline existed) and shadcn DatePicker inputs replace native date
inputs on the Schedule toolbar and TaskDetailPanel's Start Date/
Deadline fields.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 04:10:43 +08:00

122 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""
One-off script to generate example start_date/deadline data on shot tasks,
for testing/demoing the Project Schedule (Gantt) page.
Reuses the same shot-sampling approach as generate_note_example_data.py: the
dev database is heavily populated with disposable load-test fixtures, so this
picks a small, bounded sample of shots (from Dragon Quest) that have the
normal 4-task set, rather than scheduling all ~1400 shot tasks.
For each sampled shot, tasks are scheduled in pipeline order (layout ->
animation -> simulation -> lighting -> compositing, any other task types
appended after) with realistic overlapping durations, staggered around
today's date so the chart's "Today" marker falls inside the visible range.
Idempotent: skips any task that already has a start_date set, so it's safe
to re-run without clobbering manually-edited dates.
"""
from datetime import date, timedelta
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import DATABASE_URL, Base
from models.shot import Shot
from models.task import Task
from models.project import Project
import logging
import random
import models # noqa: F401 - ensures every model is registered on Base.metadata
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
PROJECT_NAME = "Dragon Quest"
SAMPLE_SIZE = 15
RNG_SEED = 42
# Canonical pipeline order used to sequence a shot's tasks; anything not
# listed here keeps its natural (query) order, appended at the end.
PIPELINE_ORDER = ["layout", "animation", "simulation", "lighting", "compositing", "previz"]
def pipeline_index(task_type: str) -> int:
return PIPELINE_ORDER.index(task_type) if task_type in PIPELINE_ORDER else len(PIPELINE_ORDER)
def generate_schedule_example_data():
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
db = SessionLocal()
rng = random.Random(RNG_SEED)
today = date.today()
try:
project = db.query(Project).filter(Project.name == PROJECT_NAME).first()
if not project:
logger.error(f"Project '{PROJECT_NAME}' not found")
return
logger.info(f"Project '{PROJECT_NAME}' (id={project.id})")
# Shots with the normal 4-task set (excludes shots with zero tasks
# and the handful of outliers with extra bulk-test tasks piled on).
shots = db.query(Shot).filter(
Shot.project_id == project.id, Shot.deleted_at.is_(None)
).order_by(Shot.id).all()
candidates = [
s for s in shots
if db.query(Task).filter(Task.shot_id == s.id, Task.deleted_at.is_(None)).count() == 4
]
sample_shots = candidates[:SAMPLE_SIZE]
logger.info(f"{len(candidates)} candidate shots with a 4-task set, sampling {len(sample_shots)}")
tasks_scheduled = 0
tasks_skipped = 0
for shot in sample_shots:
tasks = db.query(Task).filter(
Task.shot_id == shot.id, Task.deleted_at.is_(None)
).all()
tasks.sort(key=lambda t: pipeline_index(t.task_type))
# Stagger each shot's pipeline start across a window that straddles
# today, so the demo chart shows a mix of past/current/future work.
shot_start = today + timedelta(days=rng.randint(-20, 20))
cursor = shot_start
for task in tasks:
if task.start_date is not None:
tasks_skipped += 1
# Still advance the cursor so later tasks in this shot
# don't bunch up if an earlier one was already scheduled.
cursor = task.deadline or cursor
continue
duration = rng.randint(4, 12)
task.start_date = cursor
task.deadline = cursor + timedelta(days=duration)
tasks_scheduled += 1
# Next task starts a little before this one ends (realistic
# pipeline overlap) rather than strictly back-to-back.
cursor = task.deadline - timedelta(days=rng.randint(0, 4))
db.commit()
logger.info(f"Tasks scheduled: {tasks_scheduled}, skipped (already scheduled): {tasks_skipped}")
except Exception as e:
logger.error(f"Generation failed: {e}")
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
logger.info("Generating schedule example data...")
generate_schedule_example_data()
logger.info("Done!")