initial project
This commit is contained in:
84
app/template_generator.py
Normal file
84
app/template_generator.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Main entry point: builds a clash-free dummy schedule and renders all five
|
||||
timetable views (batch/department/room/student/teacher -wise) as .xlsx
|
||||
files under result/templates/<view>/.
|
||||
|
||||
Run:
|
||||
python app/template_generator.py
|
||||
"""
|
||||
|
||||
import os
|
||||
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 build_schedule # 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 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")
|
||||
|
||||
|
||||
def main():
|
||||
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()
|
||||
|
||||
print("Loading data and building clash-free schedule...")
|
||||
data, sessions = build_schedule()
|
||||
print(f"Scheduled {len(sessions)} sessions.\n")
|
||||
|
||||
print("Running conflict checks (student/batch and teacher)...")
|
||||
report = check_all_conflicts(sessions)
|
||||
if report["has_conflicts"]:
|
||||
for line in format_student_conflicts(report["student_conflicts"]):
|
||||
print(f" STUDENT CONFLICT: {line}")
|
||||
for line in format_teacher_conflicts(report["teacher_conflicts"]):
|
||||
print(f" TEACHER CONFLICT: {line}")
|
||||
print(
|
||||
f"Found {len(report['student_conflicts'])} student conflict(s) and "
|
||||
f"{len(report['teacher_conflicts'])} teacher conflict(s).\n"
|
||||
)
|
||||
else:
|
||||
print("No conflicts found — schedule is clash-free for students and teachers.\n")
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
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"\nDone. {total_files} timetable file(s) written under {RESULT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
122
app/xlsx_grid_helper.py
Normal file
122
app/xlsx_grid_helper.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Shared openpyxl helpers for rendering a day x time-slot timetable grid.
|
||||
Used by every templates/*-wise/*_template.py module so styling stays
|
||||
consistent across views.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||
|
||||
HEADER_FILL = PatternFill(start_color="1F4E78", end_color="1F4E78", fill_type="solid")
|
||||
HEADER_FONT = Font(color="FFFFFF", bold=True)
|
||||
LUNCH_FILL = PatternFill(start_color="D9D9D9", end_color="D9D9D9", fill_type="solid")
|
||||
GENERAL_FILL = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
|
||||
LAB_FILL = PatternFill(start_color="D9EAD3", end_color="D9EAD3", fill_type="solid")
|
||||
THEORY_FILL = PatternFill(start_color="DDEBF7", end_color="DDEBF7", fill_type="solid")
|
||||
THIN_BORDER = Border(
|
||||
left=Side(style="thin"), right=Side(style="thin"),
|
||||
top=Side(style="thin"), bottom=Side(style="thin"),
|
||||
)
|
||||
CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
|
||||
|
||||
def cell_fill_for(session):
|
||||
if session is None:
|
||||
return None
|
||||
if session.get("is_general"):
|
||||
return GENERAL_FILL
|
||||
if session.get("course_type") == "lab":
|
||||
return LAB_FILL
|
||||
return THEORY_FILL
|
||||
|
||||
|
||||
def build_grid_sheet(ws, title, subtitle, time_slots, sessions_by_day_slot, cell_text_fn):
|
||||
"""
|
||||
Renders one timetable grid onto worksheet `ws`.
|
||||
|
||||
time_slots: ordered list of dicts: {"label": str, "start": str, "end": str,
|
||||
"is_lunch": bool}
|
||||
sessions_by_day_slot: dict[(day, slot_index)] -> session dict (or list of
|
||||
session dicts for slots spanning a double/lab period), used
|
||||
only to decide fill color via cell_fill_for().
|
||||
cell_text_fn: function(day, slot_index) -> str to render into the cell.
|
||||
"""
|
||||
invalid_chars = ':\\/?*[]'
|
||||
sheet_name = "".join(c for c in title if c not in invalid_chars)
|
||||
ws.title = sheet_name[:31] # Excel sheet name limit
|
||||
|
||||
ws.merge_cells("A1:F1")
|
||||
ws["A1"] = title
|
||||
ws["A1"].font = Font(bold=True, size=14)
|
||||
ws["A1"].alignment = CENTER
|
||||
|
||||
if subtitle:
|
||||
ws.merge_cells("A2:F2")
|
||||
ws["A2"] = subtitle
|
||||
ws["A2"].font = Font(italic=True, size=10, color="555555")
|
||||
ws["A2"].alignment = CENTER
|
||||
|
||||
header_row = 4
|
||||
ws.cell(row=header_row, column=1, value="Time").font = HEADER_FONT
|
||||
ws.cell(row=header_row, column=1).fill = HEADER_FILL
|
||||
ws.cell(row=header_row, column=1).alignment = CENTER
|
||||
ws.cell(row=header_row, column=1).border = THIN_BORDER
|
||||
|
||||
for col, day in enumerate(DAYS, start=2):
|
||||
c = ws.cell(row=header_row, column=col, value=day)
|
||||
c.font = HEADER_FONT
|
||||
c.fill = HEADER_FILL
|
||||
c.alignment = CENTER
|
||||
c.border = THIN_BORDER
|
||||
|
||||
for row_offset, slot in enumerate(time_slots):
|
||||
row = header_row + 1 + row_offset
|
||||
time_cell = ws.cell(row=row, column=1, value=f'{slot["start"]} - {slot["end"]}')
|
||||
time_cell.font = Font(bold=True, size=9)
|
||||
time_cell.alignment = CENTER
|
||||
time_cell.border = THIN_BORDER
|
||||
if slot.get("is_lunch"):
|
||||
time_cell.fill = LUNCH_FILL
|
||||
|
||||
for col, day in enumerate(DAYS, start=2):
|
||||
cell = ws.cell(row=row, column=col)
|
||||
cell.border = THIN_BORDER
|
||||
cell.alignment = CENTER
|
||||
cell.font = Font(size=9)
|
||||
|
||||
if slot.get("is_lunch"):
|
||||
cell.fill = LUNCH_FILL
|
||||
cell.value = "LUNCH BREAK"
|
||||
continue
|
||||
|
||||
text = cell_text_fn(day, row_offset)
|
||||
cell.value = text if text else ""
|
||||
|
||||
session = sessions_by_day_slot.get((day, row_offset))
|
||||
fill = cell_fill_for(session)
|
||||
if fill:
|
||||
cell.fill = fill
|
||||
|
||||
ws.column_dimensions["A"].width = 16
|
||||
for col in range(2, 2 + len(DAYS)):
|
||||
ws.column_dimensions[get_column_letter(col)].width = 26
|
||||
|
||||
for row_offset in range(len(time_slots)):
|
||||
ws.row_dimensions[header_row + 1 + row_offset].height = 42
|
||||
|
||||
|
||||
def save_workbook(wb, output_dir, filename):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
path = os.path.join(output_dir, filename)
|
||||
wb.save(path)
|
||||
return path
|
||||
|
||||
|
||||
def new_workbook():
|
||||
wb = Workbook()
|
||||
return wb
|
||||
Reference in New Issue
Block a user