""" 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