""" Main entry point: runs the three scheduling phases (general courses, labs, remaining courses) and renders all five timetable views (batch/department/ room/student/teacher -wise) as .xlsx files under result/templates//. Each phase is its own subcommand so it can be run and inspected as a separate step; each phase's placements are persisted to result/schedule_state.json and loaded back in by the next phase's command. Usage: python app/template_generator.py phase_1 # general courses (Tue/Thu preferred) python app/template_generator.py phase_2 # labs, on top of phase_1 python app/template_generator.py phase_3 # remaining courses, batch by batch python app/template_generator.py clear_all # wipe state/results/log to start over """ import json import os import shutil import sys APP_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(APP_DIR) sys.path.insert(0, APP_DIR) sys.path.insert(0, os.path.join(ROOT_DIR, "controller")) sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "batch-wise")) sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "department-wise")) sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "room-wise")) sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "student-wise")) sys.path.insert(0, os.path.join(ROOT_DIR, "templates", "teacher-wise")) from template_controller import load_data, Scheduler # noqa: E402 from conflict_checker import check_all_conflicts # noqa: E402 from conflict_checker.student_conflict_checker import format_student_conflicts # noqa: E402 from conflict_checker.teacher_conflict_checker import format_teacher_conflicts # noqa: E402 from template_logger import reset_log, log_error, log_info, log_warning # noqa: E402 from batch_wise_template import generate_batch_wise_templates # noqa: E402 from department_wise_template import generate_department_wise_templates # noqa: E402 from room_wise_template import generate_room_wise_templates # noqa: E402 from student_wise_template import generate_student_wise_templates # noqa: E402 from teacher_wise_template import generate_teacher_wise_templates # noqa: E402 RESULT_DIR = os.path.join(ROOT_DIR, "result", "templates") STATE_FILE = os.path.join(ROOT_DIR, "result", "schedule_state.json") VIEW_NAMES = ["batch-wise", "department-wise", "room-wise", "student-wise", "teacher-wise"] PHASE_ORDER = ["phase_1", "phase_2", "phase_3"] GENERATORS = [ ("batch-wise", generate_batch_wise_templates), ("department-wise", generate_department_wise_templates), ("room-wise", generate_room_wise_templates), ("student-wise", generate_student_wise_templates), ("teacher-wise", generate_teacher_wise_templates), ] # --------------------------------------------------------------------------- # State persistence # --------------------------------------------------------------------------- def load_state(): """Returns the saved schedule state dict, or None if none exists yet.""" if not os.path.exists(STATE_FILE): return None with open(STATE_FILE, encoding="utf-8") as f: return json.load(f) def save_state(scheduler, phases_completed): state = scheduler.to_state() state["phases_completed"] = phases_completed os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, indent=2) # --------------------------------------------------------------------------- # Shared helpers # --------------------------------------------------------------------------- def _check_and_log(sessions, label): """Runs student + teacher conflict checks over `sessions` and logs the result.""" report = check_all_conflicts(sessions) if report["has_conflicts"]: for line in format_student_conflicts(report["student_conflicts"]): log_error(f"[{label}] STUDENT CONFLICT: {line}") for line in format_teacher_conflicts(report["teacher_conflicts"]): log_error(f"[{label}] TEACHER CONFLICT: {line}") log_warning( f"[{label}] Found {len(report['student_conflicts'])} student conflict(s) and " f"{len(report['teacher_conflicts'])} teacher conflict(s)." ) else: log_info(f"[{label}] No conflicts found — clash-free.") return report def clear_result_dir(): """Removes every previous run's output so stale files never linger.""" for view_name in VIEW_NAMES: output_dir = os.path.join(RESULT_DIR, view_name) if os.path.exists(output_dir): shutil.rmtree(output_dir) os.makedirs(output_dir, exist_ok=True) def ensure_dummy_data(): data_csv_marker = os.path.join(ROOT_DIR, "data", "dummy", "blocks", "rooms.csv") if not os.path.exists(data_csv_marker): print("Dummy data not found — generating it first...") sys.path.insert(0, os.path.join(ROOT_DIR, "data", "dummy")) from generate_dummy_data import main as generate_dummy_data generate_dummy_data() print() def regenerate_templates(sessions, data): clear_result_dir() total_files = 0 for view_name, generate_fn in GENERATORS: output_dir = os.path.join(RESULT_DIR, view_name) written = generate_fn(sessions, data, output_dir) total_files += len(written) print(f"[{view_name}] generated {len(written)} file(s) -> {output_dir}") print(f"\n{total_files} timetable file(s) written under {RESULT_DIR}") # --------------------------------------------------------------------------- # Commands # --------------------------------------------------------------------------- def cmd_clear_all(): if os.path.exists(STATE_FILE): os.remove(STATE_FILE) clear_result_dir() reset_log() log_info("clear_all: schedule state, result templates, and log have been reset. Run phase_1 to start over.") def cmd_phase(phase_name): ensure_dummy_data() data = load_data() state = load_state() completed = state["phases_completed"] if state else [] if phase_name in completed: log_error( f"{phase_name} has already been run (completed phases: {completed}). " f"Run 'python app/template_generator.py clear_all' first if you want to redo it." ) return phase_index = PHASE_ORDER.index(phase_name) required_prior = PHASE_ORDER[:phase_index] missing = [p for p in required_prior if p not in completed] if missing: log_error( f"Cannot run {phase_name} yet — missing prior phase(s) {missing}. " f"Run them in order: {', '.join(PHASE_ORDER)} (or 'clear_all' to start over)." ) return if state is None: scheduler = Scheduler(data) else: scheduler = Scheduler.from_state(data, state) if phase_name == "phase_1": log_info("Phase 1/3: scheduling general courses (Mon/Wed preferred)...") new_sessions = scheduler.phase_1_general_courses() elif phase_name == "phase_2": log_info("Phase 2/3: scheduling labs (on top of phase_1's fixed placements)...") new_sessions = scheduler.phase_2_labs() else: log_info("Phase 3/3: scheduling remaining courses (batch by batch)...") new_sessions = scheduler.phase_3_remaining_courses() label = { "phase_1": "Phase 1 (general courses)", "phase_2": "Phase 2 (general courses + labs)", "phase_3": "Phase 3 (full schedule)", }[phase_name] _check_and_log(scheduler.sessions, label) completed = completed + [phase_name] save_state(scheduler, completed) log_info( f"{phase_name} complete: {len(new_sessions)} session(s) added this phase, " f"{len(scheduler.sessions)} total so far. State saved to {STATE_FILE}." ) regenerate_templates(scheduler.sessions, data) def print_usage(): print(__doc__) def main(): args = sys.argv[1:] if not args or args[0] not in ("phase_1", "phase_2", "phase_3", "clear_all"): print_usage() return command = args[0] if command == "clear_all": cmd_clear_all() else: cmd_phase(command) if __name__ == "__main__": main()