commit a7bb2f3dae6c61a91f37a72b766be1055b671f59 Author: Talhadeveloperr <23021519-005@uog.edu.pk> Date: Wed Sep 9 17:00:54 2026 +0500 initial project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/app/template_generator.py b/app/template_generator.py new file mode 100644 index 0000000..5b005c6 --- /dev/null +++ b/app/template_generator.py @@ -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//. + +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() diff --git a/app/xlsx_grid_helper.py b/app/xlsx_grid_helper.py new file mode 100644 index 0000000..5fa33a1 --- /dev/null +++ b/app/xlsx_grid_helper.py @@ -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 diff --git a/controller/conflict_checker/__init__.py b/controller/conflict_checker/__init__.py new file mode 100644 index 0000000..04fe44d --- /dev/null +++ b/controller/conflict_checker/__init__.py @@ -0,0 +1,19 @@ +from .student_conflict_checker import check_student_conflicts +from .teacher_conflict_checker import check_teacher_conflicts + +__all__ = ["check_student_conflicts", "check_teacher_conflicts", "check_all_conflicts"] + + +def check_all_conflicts(sessions): + """ + Runs every conflict check against the given list of scheduled sessions + and returns a combined report: + {"student_conflicts": [...], "teacher_conflicts": [...], "has_conflicts": bool} + """ + student_conflicts = check_student_conflicts(sessions) + teacher_conflicts = check_teacher_conflicts(sessions) + return { + "student_conflicts": student_conflicts, + "teacher_conflicts": teacher_conflicts, + "has_conflicts": bool(student_conflicts or teacher_conflicts), + } diff --git a/controller/conflict_checker/student_conflict_checker.py b/controller/conflict_checker/student_conflict_checker.py new file mode 100644 index 0000000..814661f --- /dev/null +++ b/controller/conflict_checker/student_conflict_checker.py @@ -0,0 +1,63 @@ +""" +Verifies that no batch (and therefore no student in that batch) is booked +into two courses — lecture or lab — at the same day+slot. + +This is an independent verification pass over the scheduler's output: it +does not trust the scheduler's own busy-maps, it re-derives every batch's +occupied (day, slot_index) cells directly from the session list and flags +any cell used by more than one session. +""" + +from collections import defaultdict + + +def check_student_conflicts(sessions): + """ + Returns a list of conflict dicts, one per (batch, day, slot) where two + or more sessions overlap for that batch: + { + "batch_code": str, + "day": str, + "slot_index": int, + "sessions": [session, session, ...], + } + Sessions that span multiple slot_indices (labs) occupy every one of + their slot_indices, so a lab overlapping a lecture in either of its two + slots is caught too. + """ + # (batch_code, day, slot_index) -> list of sessions occupying that cell + occupied = defaultdict(list) + + for session in sessions: + day = session["day"] + for slot_index in session["slot_indices"]: + for batch_code in session["batch_codes"]: + occupied[(batch_code, day, slot_index)].append(session) + + conflicts = [] + for (batch_code, day, slot_index), cell_sessions in occupied.items(): + if len(cell_sessions) > 1: + conflicts.append({ + "batch_code": batch_code, + "day": day, + "slot_index": slot_index, + "sessions": cell_sessions, + }) + + conflicts.sort(key=lambda c: (c["batch_code"], c["day"], c["slot_index"])) + return conflicts + + +def format_student_conflicts(conflicts): + """Human-readable lines describing each student/batch conflict.""" + lines = [] + for c in conflicts: + course_list = ", ".join( + f'{s["course_id"]} ({s["course_type"]}) with {s["instructor_name"]} in {s["room_id"]}' + for s in c["sessions"] + ) + lines.append( + f'Batch {c["batch_code"]} has {len(c["sessions"])} overlapping courses on ' + f'{c["day"]} slot {c["slot_index"]}: {course_list}' + ) + return lines diff --git a/controller/conflict_checker/teacher_conflict_checker.py b/controller/conflict_checker/teacher_conflict_checker.py new file mode 100644 index 0000000..aecfb24 --- /dev/null +++ b/controller/conflict_checker/teacher_conflict_checker.py @@ -0,0 +1,59 @@ +""" +Verifies that no instructor is booked into two sessions at the same +day+slot (whether both are lectures, both labs, or a mix of either). + +Like student_conflict_checker, this re-derives occupancy directly from the +session list rather than trusting the scheduler's internal busy-maps. +""" + +from collections import defaultdict + + +def check_teacher_conflicts(sessions): + """ + Returns a list of conflict dicts, one per (instructor_code, day, slot) + where two or more sessions overlap for that instructor: + { + "instructor_code": str, + "day": str, + "slot_index": int, + "sessions": [session, session, ...], + } + """ + # (instructor_code, day, slot_index) -> list of sessions occupying that cell + occupied = defaultdict(list) + + for session in sessions: + day = session["day"] + instructor_code = session["instructor_code"] + for slot_index in session["slot_indices"]: + occupied[(instructor_code, day, slot_index)].append(session) + + conflicts = [] + for (instructor_code, day, slot_index), cell_sessions in occupied.items(): + if len(cell_sessions) > 1: + conflicts.append({ + "instructor_code": instructor_code, + "day": day, + "slot_index": slot_index, + "sessions": cell_sessions, + }) + + conflicts.sort(key=lambda c: (c["instructor_code"], c["day"], c["slot_index"])) + return conflicts + + +def format_teacher_conflicts(conflicts): + """Human-readable lines describing each teacher conflict.""" + lines = [] + for c in conflicts: + course_list = ", ".join( + f'{s["course_id"]} ({s["course_type"]}) for batch(es) {"+".join(s["batch_codes"])} in {s["room_id"]}' + for s in c["sessions"] + ) + instructor_name = c["sessions"][0]["instructor_name"] + lines.append( + f'Instructor {instructor_name} ({c["instructor_code"]}) is double-booked on ' + f'{c["day"]} slot {c["slot_index"]}: {course_list}' + ) + return lines diff --git a/controller/template_controller.py b/controller/template_controller.py new file mode 100644 index 0000000..ca7def1 --- /dev/null +++ b/controller/template_controller.py @@ -0,0 +1,206 @@ +""" +Loads dummy CSV data and builds a clash-free schedule of sessions that all +five templates/*-wise template modules render from. + +Scheduling rules (dummy/demo scheduler, not a full CSP solver): + 1. General courses (shared across every batch of a session) are placed + first, only on Tuesday, so those slots are locked in before anything + else and never clash across the batches sharing them. + 2. Lab courses (180-minute / double-slot) are placed next, in lab rooms + only, on whichever day/slot-pair is free for the batch, instructor, + and room. + 3. Remaining theory courses fill the rest of the week (Mon/Tue/Wed/Thu/Fri), + one 90-minute slot at a time, avoiding room / instructor / batch clashes. +""" + +import csv +import os + +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "dummy") + +DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] + +# Theory grid: 5 x 90-minute slots with a lunch break, 08:30-16:30 +TIME_SLOTS = [ + {"label": "S1", "start": "08:30", "end": "10:00", "is_lunch": False}, + {"label": "S2", "start": "10:00", "end": "11:30", "is_lunch": False}, + {"label": "S3", "start": "11:30", "end": "13:00", "is_lunch": False}, + {"label": "LUNCH", "start": "13:00", "end": "13:30", "is_lunch": True}, + {"label": "S4", "start": "13:30", "end": "15:00", "is_lunch": False}, + {"label": "S5", "start": "15:00", "end": "16:30", "is_lunch": False}, +] + +# Indices into TIME_SLOTS that are real teaching slots (skip lunch) +TEACHING_SLOT_INDICES = [i for i, s in enumerate(TIME_SLOTS) if not s["is_lunch"]] + +# Consecutive pairs of teaching slots usable as a 180-minute lab double-slot +LAB_SLOT_PAIRS = [ + (a, b) + for a, b in zip(TEACHING_SLOT_INDICES, TEACHING_SLOT_INDICES[1:]) + if TIME_SLOTS[b]["start"] == TIME_SLOTS[a]["end"] +] + + +def _read_csv(relative_path): + path = os.path.join(DATA_DIR, relative_path) + with open(path, newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +def load_data(): + """Loads all dummy CSVs into plain dict/list structures.""" + rooms = _read_csv(os.path.join("blocks", "rooms.csv")) + blocks = _read_csv(os.path.join("blocks", "blocks.csv")) + instructors = _read_csv(os.path.join("instructor", "instructors.csv")) + courses = _read_csv(os.path.join("courses", "courses.csv")) + offerings = _read_csv(os.path.join("courses", "course_offering.csv")) + students = _read_csv(os.path.join("roll_no", "students.csv")) + batches_25 = _read_csv(os.path.join("batch_codes", "25", "batches.csv")) + batches_26 = _read_csv(os.path.join("batch_codes", "26", "batches.csv")) + batches = batches_25 + batches_26 + + courses_by_id = {c["course_id"]: c for c in courses} + instructors_by_code = {i["instructor_code"]: i for i in instructors} + batches_by_code = {b["batch_code"]: b for b in batches} + + # normalize offerings: expand "batch1|batch2" general offerings into a + # list of batch codes on each offering record + for off in offerings: + off["batch_codes"] = off["batch_code"].split("|") + off["is_general"] = courses_by_id[off["course_id"]]["is_general"].lower() == "yes" + off["course_type"] = courses_by_id[off["course_id"]]["course_type"] + + return { + "rooms": rooms, + "blocks": blocks, + "instructors": instructors, + "instructors_by_code": instructors_by_code, + "courses": courses, + "courses_by_id": courses_by_id, + "offerings": offerings, + "students": students, + "batches": batches, + "batches_by_code": batches_by_code, + } + + +class Scheduler: + """Greedy clash-free scheduler over rooms / instructors / batches.""" + + def __init__(self, data): + self.data = data + self.classrooms = [r for r in data["rooms"] if r["room_type"] == "classroom"] + self.labs = [r for r in data["rooms"] if r["room_type"] == "lab"] + + # busy[(day, slot_index)] -> set of keys already occupied + self.room_busy = {} + self.instructor_busy = {} + self.batch_busy = {} + + self.sessions = [] + + def _is_free(self, day, slot_index, room_id, instructor_code, batch_codes): + key = (day, slot_index) + if room_id in self.room_busy.get(key, set()): + return False + if instructor_code in self.instructor_busy.get(key, set()): + return False + if any(b in self.batch_busy.get(key, set()) for b in batch_codes): + return False + return True + + def _mark_busy(self, day, slot_index, room_id, instructor_code, batch_codes): + key = (day, slot_index) + self.room_busy.setdefault(key, set()).add(room_id) + self.instructor_busy.setdefault(key, set()).add(instructor_code) + self.batch_busy.setdefault(key, set()).update(batch_codes) + + def _add_session(self, offering, day, slot_indices, room): + course = self.data["courses_by_id"][offering["course_id"]] + start = TIME_SLOTS[slot_indices[0]]["start"] + end = TIME_SLOTS[slot_indices[-1]]["end"] + self.sessions.append({ + "day": day, + "slot_indices": slot_indices, + "start": start, + "end": end, + "course_id": offering["course_id"], + "course_name": course["course_name"], + "course_type": course["course_type"], + "is_general": offering["is_general"], + "instructor_code": offering["instructor_code"], + "instructor_name": self.data["instructors_by_code"][offering["instructor_code"]]["instructor_name"], + "room_id": room["room_id"], + "room_name": room["room_name"], + "batch_codes": offering["batch_codes"], + }) + + def schedule_general_courses(self): + day = "Tuesday" + general = [o for o in self.data["offerings"] if o["is_general"]] + for offering in general: + placed = False + for slot_index in TEACHING_SLOT_INDICES: + for room in self.classrooms: + if self._is_free(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]): + self._mark_busy(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]) + self._add_session(offering, day, [slot_index], room) + placed = True + break + if placed: + break + if not placed: + print(f"WARNING: could not place general course {offering['course_id']}") + + def schedule_lab_courses(self): + labs_offerings = [o for o in self.data["offerings"] if o["course_type"] == "lab" and not o["is_general"]] + for offering in labs_offerings: + placed = False + for day in DAYS: + for slot_a, slot_b in LAB_SLOT_PAIRS: + for room in self.labs: + if self._is_free(day, slot_a, room["room_id"], offering["instructor_code"], offering["batch_codes"]) and \ + self._is_free(day, slot_b, room["room_id"], offering["instructor_code"], offering["batch_codes"]): + self._mark_busy(day, slot_a, room["room_id"], offering["instructor_code"], offering["batch_codes"]) + self._mark_busy(day, slot_b, room["room_id"], offering["instructor_code"], offering["batch_codes"]) + self._add_session(offering, day, [slot_a, slot_b], room) + placed = True + break + if placed: + break + if placed: + break + if not placed: + print(f"WARNING: could not place lab course {offering['course_id']} for {offering['batch_codes']}") + + def schedule_theory_courses(self): + theory_offerings = [o for o in self.data["offerings"] if o["course_type"] == "theory" and not o["is_general"]] + for offering in theory_offerings: + placed = False + for day in DAYS: + for slot_index in TEACHING_SLOT_INDICES: + for room in self.classrooms: + if self._is_free(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]): + self._mark_busy(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]) + self._add_session(offering, day, [slot_index], room) + placed = True + break + if placed: + break + if placed: + break + if not placed: + print(f"WARNING: could not place theory course {offering['course_id']} for {offering['batch_codes']}") + + def run(self): + self.schedule_general_courses() + self.schedule_lab_courses() + self.schedule_theory_courses() + return self.sessions + + +def build_schedule(): + data = load_data() + scheduler = Scheduler(data) + sessions = scheduler.run() + return data, sessions diff --git a/data/dummy/batch_codes/25/batches.csv b/data/dummy/batch_codes/25/batches.csv new file mode 100644 index 0000000..99fb6f5 --- /dev/null +++ b/data/dummy/batch_codes/25/batches.csv @@ -0,0 +1,4 @@ +batch_code,department,program,session +25021519,CS,BSCS,25 +25021520,CS,BSSE,25 +25031519,EE,BSEE,25 diff --git a/data/dummy/batch_codes/26/batches.csv b/data/dummy/batch_codes/26/batches.csv new file mode 100644 index 0000000..ef25f8a --- /dev/null +++ b/data/dummy/batch_codes/26/batches.csv @@ -0,0 +1,4 @@ +batch_code,department,program,session +26021519,CS,BSCS,26 +26021520,CS,BSSE,26 +26031519,EE,BSEE,26 diff --git a/data/dummy/blocks/blocks.csv b/data/dummy/blocks/blocks.csv new file mode 100644 index 0000000..cc42f81 --- /dev/null +++ b/data/dummy/blocks/blocks.csv @@ -0,0 +1,2 @@ +block_id,block_name +A,Block A diff --git a/data/dummy/blocks/rooms.csv b/data/dummy/blocks/rooms.csv new file mode 100644 index 0000000..5306ce8 --- /dev/null +++ b/data/dummy/blocks/rooms.csv @@ -0,0 +1,7 @@ +room_id,room_name,block_id,room_type,capacity +A-101,Room 101,A,classroom,60 +A-102,Room 102,A,classroom,60 +A-103,Room 103,A,classroom,50 +A-201,Room 201,A,classroom,60 +A-L01,Computer Lab 1,A,lab,35 +A-L02,Computer Lab 2,A,lab,35 diff --git a/data/dummy/courses/course_offering.csv b/data/dummy/courses/course_offering.csv new file mode 100644 index 0000000..bb12df3 --- /dev/null +++ b/data/dummy/courses/course_offering.csv @@ -0,0 +1,25 @@ +course_id,batch_code,instructor_code +CS101,25021519,1001 +CS102,25021519,1001 +CS201,25021519,1002 +CS202,25021519,1002 +CS101,25021520,1001 +CS102,25021520,1001 +CS201,25021520,1002 +CS202,25021520,1002 +EE101,25031519,1004 +EE102,25031519,1004 +CS101,26021519,1001 +CS102,26021519,1001 +CS201,26021519,1002 +CS202,26021519,1002 +CS101,26021520,1001 +CS102,26021520,1001 +CS201,26021520,1002 +CS202,26021520,1002 +EE101,26031519,1004 +EE102,26031519,1004 +GEN101,25021519|25021520|25031519,1002 +GEN102,25021519|25021520|25031519,1006 +GEN101,26021519|26021520|26031519,1002 +GEN102,26021519|26021520|26031519,1006 diff --git a/data/dummy/courses/courses.csv b/data/dummy/courses/courses.csv new file mode 100644 index 0000000..a88d24b --- /dev/null +++ b/data/dummy/courses/courses.csv @@ -0,0 +1,9 @@ +course_id,course_name,course_type,credit_hours,is_general +CS101,Programming Fundamentals,theory,3,no +CS102,Programming Fundamentals Lab,lab,1,no +CS201,Data Structures,theory,3,no +CS202,Data Structures Lab,lab,1,no +EE101,Basic Electronics,theory,3,no +EE102,Basic Electronics Lab,lab,1,no +GEN101,Functional English,theory,3,yes +GEN102,Islamic Studies,theory,2,yes diff --git a/data/dummy/generate_dummy_data.py b/data/dummy/generate_dummy_data.py new file mode 100644 index 0000000..3ca3cd7 --- /dev/null +++ b/data/dummy/generate_dummy_data.py @@ -0,0 +1,160 @@ +""" +Generates small illustrative dummy CSV datasets under data/dummy/ so the +timetable template generator has something to schedule and render. + +Run once: + python data/dummy/generate_dummy_data.py +""" + +import csv +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def write_csv(relative_path, header, rows): + path = os.path.join(BASE_DIR, relative_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(header) + writer.writerows(rows) + print(f"wrote {len(rows)} rows -> {path}") + + +def main(): + # ---- blocks & rooms (Building/Block A only) ---- + write_csv( + "blocks/blocks.csv", + ["block_id", "block_name"], + [["A", "Block A"]], + ) + + write_csv( + "blocks/rooms.csv", + ["room_id", "room_name", "block_id", "room_type", "capacity"], + [ + ["A-101", "Room 101", "A", "classroom", 60], + ["A-102", "Room 102", "A", "classroom", 60], + ["A-103", "Room 103", "A", "classroom", 50], + ["A-201", "Room 201", "A", "classroom", 60], + ["A-L01", "Computer Lab 1", "A", "lab", 35], + ["A-L02", "Computer Lab 2", "A", "lab", 35], + ], + ) + + # ---- departments / programs / batches ---- + # batch_code = session(2) + program_id(6) e.g. 25021519 + batches_25 = [ + ["25021519", "CS", "BSCS", "25"], + ["25021520", "CS", "BSSE", "25"], + ["25031519", "EE", "BSEE", "25"], + ] + batches_26 = [ + ["26021519", "CS", "BSCS", "26"], + ["26021520", "CS", "BSSE", "26"], + ["26031519", "EE", "BSEE", "26"], + ] + + write_csv( + "batch_codes/25/batches.csv", + ["batch_code", "department", "program", "session"], + batches_25, + ) + write_csv( + "batch_codes/26/batches.csv", + ["batch_code", "department", "program", "session"], + batches_26, + ) + + all_batches = batches_25 + batches_26 + + # ---- instructors (4-digit employee codes) ---- + instructors = [ + ["1001", "Dr. Ahsan Raza", "CS"], + ["1002", "Ms. Sana Tariq", "CS"], + ["1003", "Mr. Bilal Khan", "CS"], + ["1004", "Dr. Farah Iqbal", "EE"], + ["1005", "Mr. Usman Ali", "EE"], + ["1006", "Ms. Hira Shah", "CS"], + ] + write_csv( + "instructor/instructors.csv", + ["instructor_code", "instructor_name", "department"], + instructors, + ) + + # ---- courses ---- + # is_general courses are shared across all batches of the same session + # (so they can be scheduled together on Tuesday with no clash). + courses = [ + ["CS101", "Programming Fundamentals", "theory", 3, "no"], + ["CS102", "Programming Fundamentals Lab", "lab", 1, "no"], + ["CS201", "Data Structures", "theory", 3, "no"], + ["CS202", "Data Structures Lab", "lab", 1, "no"], + ["EE101", "Basic Electronics", "theory", 3, "no"], + ["EE102", "Basic Electronics Lab", "lab", 1, "no"], + ["GEN101", "Functional English", "theory", 3, "yes"], + ["GEN102", "Islamic Studies", "theory", 2, "yes"], + ] + write_csv( + "courses/courses.csv", + ["course_id", "course_name", "course_type", "credit_hours", "is_general"], + courses, + ) + + # ---- course offerings: which course is taught to which batch, by whom ---- + # General courses (GEN101/GEN102) are offered once per session (25/26) + # and shared across every batch in that session. + offerings = [] + + dept_theory_lab = { + "CS": [("CS101", "CS102"), ("CS201", "CS202")], + "EE": [("EE101", "EE102")], + } + dept_instructors = { + "CS": ["1001", "1002", "1003", "1006"], + "EE": ["1004", "1005"], + } + + for batch_code, department, program, session in all_batches: + pairs = dept_theory_lab[department] + instr_pool = dept_instructors[department] + for i, (theory_id, lab_id) in enumerate(pairs): + instr = instr_pool[i % len(instr_pool)] + offerings.append([theory_id, batch_code, instr]) + offerings.append([lab_id, batch_code, instr]) + + # general courses: one offering per (course, session), batch_group lists + # every batch code sharing that session, separated by "|" + for session, batch_group in ( + ("25", [b[0] for b in batches_25]), + ("26", [b[0] for b in batches_26]), + ): + offerings.append(["GEN101", "|".join(batch_group), "1002"]) + offerings.append(["GEN102", "|".join(batch_group), "1006"]) + + write_csv( + "courses/course_offering.csv", + ["course_id", "batch_code", "instructor_code"], + offerings, + ) + + # ---- students (roll_no = batch_code-seq) ---- + students = [] + for batch_code, department, program, session in all_batches: + for seq in range(1, 6): # 5 sample students per batch + roll_no = f"{batch_code}-{seq:03d}" + students.append([roll_no, batch_code, f"Student {roll_no}"]) + + write_csv( + "roll_no/students.csv", + ["roll_no", "batch_code", "student_name"], + students, + ) + + print("\nDummy data generation complete.") + + +if __name__ == "__main__": + main() diff --git a/data/dummy/instructor/instructors.csv b/data/dummy/instructor/instructors.csv new file mode 100644 index 0000000..0ff9b94 --- /dev/null +++ b/data/dummy/instructor/instructors.csv @@ -0,0 +1,7 @@ +instructor_code,instructor_name,department +1001,Dr. Ahsan Raza,CS +1002,Ms. Sana Tariq,CS +1003,Mr. Bilal Khan,CS +1004,Dr. Farah Iqbal,EE +1005,Mr. Usman Ali,EE +1006,Ms. Hira Shah,CS diff --git a/data/dummy/roll_no/students.csv b/data/dummy/roll_no/students.csv new file mode 100644 index 0000000..31327da --- /dev/null +++ b/data/dummy/roll_no/students.csv @@ -0,0 +1,31 @@ +roll_no,batch_code,student_name +25021519-001,25021519,Student 25021519-001 +25021519-002,25021519,Student 25021519-002 +25021519-003,25021519,Student 25021519-003 +25021519-004,25021519,Student 25021519-004 +25021519-005,25021519,Student 25021519-005 +25021520-001,25021520,Student 25021520-001 +25021520-002,25021520,Student 25021520-002 +25021520-003,25021520,Student 25021520-003 +25021520-004,25021520,Student 25021520-004 +25021520-005,25021520,Student 25021520-005 +25031519-001,25031519,Student 25031519-001 +25031519-002,25031519,Student 25031519-002 +25031519-003,25031519,Student 25031519-003 +25031519-004,25031519,Student 25031519-004 +25031519-005,25031519,Student 25031519-005 +26021519-001,26021519,Student 26021519-001 +26021519-002,26021519,Student 26021519-002 +26021519-003,26021519,Student 26021519-003 +26021519-004,26021519,Student 26021519-004 +26021519-005,26021519,Student 26021519-005 +26021520-001,26021520,Student 26021520-001 +26021520-002,26021520,Student 26021520-002 +26021520-003,26021520,Student 26021520-003 +26021520-004,26021520,Student 26021520-004 +26021520-005,26021520,Student 26021520-005 +26031519-001,26031519,Student 26031519-001 +26031519-002,26031519,Student 26031519-002 +26031519-003,26031519,Student 26031519-003 +26031519-004,26031519,Student 26031519-004 +26031519-005,26031519,Student 26031519-005 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..289437c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +openpyxl>=3.1 diff --git a/result/templates/batch-wise/25021519.xlsx b/result/templates/batch-wise/25021519.xlsx new file mode 100644 index 0000000..c0c45fa Binary files /dev/null and b/result/templates/batch-wise/25021519.xlsx differ diff --git a/result/templates/batch-wise/25021520.xlsx b/result/templates/batch-wise/25021520.xlsx new file mode 100644 index 0000000..733bcd6 Binary files /dev/null and b/result/templates/batch-wise/25021520.xlsx differ diff --git a/result/templates/batch-wise/25031519.xlsx b/result/templates/batch-wise/25031519.xlsx new file mode 100644 index 0000000..f208379 Binary files /dev/null and b/result/templates/batch-wise/25031519.xlsx differ diff --git a/result/templates/batch-wise/26021519.xlsx b/result/templates/batch-wise/26021519.xlsx new file mode 100644 index 0000000..2a42823 Binary files /dev/null and b/result/templates/batch-wise/26021519.xlsx differ diff --git a/result/templates/batch-wise/26021520.xlsx b/result/templates/batch-wise/26021520.xlsx new file mode 100644 index 0000000..b8c5f69 Binary files /dev/null and b/result/templates/batch-wise/26021520.xlsx differ diff --git a/result/templates/batch-wise/26031519.xlsx b/result/templates/batch-wise/26031519.xlsx new file mode 100644 index 0000000..5b7bca2 Binary files /dev/null and b/result/templates/batch-wise/26031519.xlsx differ diff --git a/result/templates/department-wise/CS.xlsx b/result/templates/department-wise/CS.xlsx new file mode 100644 index 0000000..a5d9e65 Binary files /dev/null and b/result/templates/department-wise/CS.xlsx differ diff --git a/result/templates/department-wise/EE.xlsx b/result/templates/department-wise/EE.xlsx new file mode 100644 index 0000000..016287b Binary files /dev/null and b/result/templates/department-wise/EE.xlsx differ diff --git a/result/templates/room-wise/A-101.xlsx b/result/templates/room-wise/A-101.xlsx new file mode 100644 index 0000000..9fab134 Binary files /dev/null and b/result/templates/room-wise/A-101.xlsx differ diff --git a/result/templates/room-wise/A-102.xlsx b/result/templates/room-wise/A-102.xlsx new file mode 100644 index 0000000..c6c4dda Binary files /dev/null and b/result/templates/room-wise/A-102.xlsx differ diff --git a/result/templates/room-wise/A-103.xlsx b/result/templates/room-wise/A-103.xlsx new file mode 100644 index 0000000..0289c41 Binary files /dev/null and b/result/templates/room-wise/A-103.xlsx differ diff --git a/result/templates/room-wise/A-201.xlsx b/result/templates/room-wise/A-201.xlsx new file mode 100644 index 0000000..a2dbce8 Binary files /dev/null and b/result/templates/room-wise/A-201.xlsx differ diff --git a/result/templates/room-wise/A-L01.xlsx b/result/templates/room-wise/A-L01.xlsx new file mode 100644 index 0000000..249958c Binary files /dev/null and b/result/templates/room-wise/A-L01.xlsx differ diff --git a/result/templates/room-wise/A-L02.xlsx b/result/templates/room-wise/A-L02.xlsx new file mode 100644 index 0000000..83626f4 Binary files /dev/null and b/result/templates/room-wise/A-L02.xlsx differ diff --git a/result/templates/student-wise/25021519-001.xlsx b/result/templates/student-wise/25021519-001.xlsx new file mode 100644 index 0000000..96f2c18 Binary files /dev/null and b/result/templates/student-wise/25021519-001.xlsx differ diff --git a/result/templates/student-wise/25021519-002.xlsx b/result/templates/student-wise/25021519-002.xlsx new file mode 100644 index 0000000..893be02 Binary files /dev/null and b/result/templates/student-wise/25021519-002.xlsx differ diff --git a/result/templates/student-wise/25021519-003.xlsx b/result/templates/student-wise/25021519-003.xlsx new file mode 100644 index 0000000..7e4e585 Binary files /dev/null and b/result/templates/student-wise/25021519-003.xlsx differ diff --git a/result/templates/student-wise/25021519-004.xlsx b/result/templates/student-wise/25021519-004.xlsx new file mode 100644 index 0000000..c803a24 Binary files /dev/null and b/result/templates/student-wise/25021519-004.xlsx differ diff --git a/result/templates/student-wise/25021519-005.xlsx b/result/templates/student-wise/25021519-005.xlsx new file mode 100644 index 0000000..1f54fc6 Binary files /dev/null and b/result/templates/student-wise/25021519-005.xlsx differ diff --git a/result/templates/student-wise/25021520-001.xlsx b/result/templates/student-wise/25021520-001.xlsx new file mode 100644 index 0000000..5e20a9f Binary files /dev/null and b/result/templates/student-wise/25021520-001.xlsx differ diff --git a/result/templates/student-wise/25021520-002.xlsx b/result/templates/student-wise/25021520-002.xlsx new file mode 100644 index 0000000..c97387d Binary files /dev/null and b/result/templates/student-wise/25021520-002.xlsx differ diff --git a/result/templates/student-wise/25021520-003.xlsx b/result/templates/student-wise/25021520-003.xlsx new file mode 100644 index 0000000..a8eac05 Binary files /dev/null and b/result/templates/student-wise/25021520-003.xlsx differ diff --git a/result/templates/student-wise/25021520-004.xlsx b/result/templates/student-wise/25021520-004.xlsx new file mode 100644 index 0000000..cc1ea7a Binary files /dev/null and b/result/templates/student-wise/25021520-004.xlsx differ diff --git a/result/templates/student-wise/25021520-005.xlsx b/result/templates/student-wise/25021520-005.xlsx new file mode 100644 index 0000000..dda79bf Binary files /dev/null and b/result/templates/student-wise/25021520-005.xlsx differ diff --git a/result/templates/student-wise/25031519-001.xlsx b/result/templates/student-wise/25031519-001.xlsx new file mode 100644 index 0000000..4c3b9d2 Binary files /dev/null and b/result/templates/student-wise/25031519-001.xlsx differ diff --git a/result/templates/student-wise/25031519-002.xlsx b/result/templates/student-wise/25031519-002.xlsx new file mode 100644 index 0000000..1b972a9 Binary files /dev/null and b/result/templates/student-wise/25031519-002.xlsx differ diff --git a/result/templates/student-wise/25031519-003.xlsx b/result/templates/student-wise/25031519-003.xlsx new file mode 100644 index 0000000..11f5ee9 Binary files /dev/null and b/result/templates/student-wise/25031519-003.xlsx differ diff --git a/result/templates/student-wise/25031519-004.xlsx b/result/templates/student-wise/25031519-004.xlsx new file mode 100644 index 0000000..139e5b3 Binary files /dev/null and b/result/templates/student-wise/25031519-004.xlsx differ diff --git a/result/templates/student-wise/25031519-005.xlsx b/result/templates/student-wise/25031519-005.xlsx new file mode 100644 index 0000000..3ef170a Binary files /dev/null and b/result/templates/student-wise/25031519-005.xlsx differ diff --git a/result/templates/student-wise/26021519-001.xlsx b/result/templates/student-wise/26021519-001.xlsx new file mode 100644 index 0000000..2623176 Binary files /dev/null and b/result/templates/student-wise/26021519-001.xlsx differ diff --git a/result/templates/student-wise/26021519-002.xlsx b/result/templates/student-wise/26021519-002.xlsx new file mode 100644 index 0000000..04ec9e8 Binary files /dev/null and b/result/templates/student-wise/26021519-002.xlsx differ diff --git a/result/templates/student-wise/26021519-003.xlsx b/result/templates/student-wise/26021519-003.xlsx new file mode 100644 index 0000000..d4e3e97 Binary files /dev/null and b/result/templates/student-wise/26021519-003.xlsx differ diff --git a/result/templates/student-wise/26021519-004.xlsx b/result/templates/student-wise/26021519-004.xlsx new file mode 100644 index 0000000..1369ccf Binary files /dev/null and b/result/templates/student-wise/26021519-004.xlsx differ diff --git a/result/templates/student-wise/26021519-005.xlsx b/result/templates/student-wise/26021519-005.xlsx new file mode 100644 index 0000000..0dc3de6 Binary files /dev/null and b/result/templates/student-wise/26021519-005.xlsx differ diff --git a/result/templates/student-wise/26021520-001.xlsx b/result/templates/student-wise/26021520-001.xlsx new file mode 100644 index 0000000..605adcf Binary files /dev/null and b/result/templates/student-wise/26021520-001.xlsx differ diff --git a/result/templates/student-wise/26021520-002.xlsx b/result/templates/student-wise/26021520-002.xlsx new file mode 100644 index 0000000..9364a3c Binary files /dev/null and b/result/templates/student-wise/26021520-002.xlsx differ diff --git a/result/templates/student-wise/26021520-003.xlsx b/result/templates/student-wise/26021520-003.xlsx new file mode 100644 index 0000000..17a7bb8 Binary files /dev/null and b/result/templates/student-wise/26021520-003.xlsx differ diff --git a/result/templates/student-wise/26021520-004.xlsx b/result/templates/student-wise/26021520-004.xlsx new file mode 100644 index 0000000..1e6fb56 Binary files /dev/null and b/result/templates/student-wise/26021520-004.xlsx differ diff --git a/result/templates/student-wise/26021520-005.xlsx b/result/templates/student-wise/26021520-005.xlsx new file mode 100644 index 0000000..6d06cac Binary files /dev/null and b/result/templates/student-wise/26021520-005.xlsx differ diff --git a/result/templates/student-wise/26031519-001.xlsx b/result/templates/student-wise/26031519-001.xlsx new file mode 100644 index 0000000..ec5cf0f Binary files /dev/null and b/result/templates/student-wise/26031519-001.xlsx differ diff --git a/result/templates/student-wise/26031519-002.xlsx b/result/templates/student-wise/26031519-002.xlsx new file mode 100644 index 0000000..d72aab3 Binary files /dev/null and b/result/templates/student-wise/26031519-002.xlsx differ diff --git a/result/templates/student-wise/26031519-003.xlsx b/result/templates/student-wise/26031519-003.xlsx new file mode 100644 index 0000000..3f20c01 Binary files /dev/null and b/result/templates/student-wise/26031519-003.xlsx differ diff --git a/result/templates/student-wise/26031519-004.xlsx b/result/templates/student-wise/26031519-004.xlsx new file mode 100644 index 0000000..ccd07b6 Binary files /dev/null and b/result/templates/student-wise/26031519-004.xlsx differ diff --git a/result/templates/student-wise/26031519-005.xlsx b/result/templates/student-wise/26031519-005.xlsx new file mode 100644 index 0000000..e64dc11 Binary files /dev/null and b/result/templates/student-wise/26031519-005.xlsx differ diff --git a/result/templates/teacher-wise/1001.xlsx b/result/templates/teacher-wise/1001.xlsx new file mode 100644 index 0000000..a829e6e Binary files /dev/null and b/result/templates/teacher-wise/1001.xlsx differ diff --git a/result/templates/teacher-wise/1002.xlsx b/result/templates/teacher-wise/1002.xlsx new file mode 100644 index 0000000..4228c52 Binary files /dev/null and b/result/templates/teacher-wise/1002.xlsx differ diff --git a/result/templates/teacher-wise/1003.xlsx b/result/templates/teacher-wise/1003.xlsx new file mode 100644 index 0000000..37fca81 Binary files /dev/null and b/result/templates/teacher-wise/1003.xlsx differ diff --git a/result/templates/teacher-wise/1004.xlsx b/result/templates/teacher-wise/1004.xlsx new file mode 100644 index 0000000..53eb778 Binary files /dev/null and b/result/templates/teacher-wise/1004.xlsx differ diff --git a/result/templates/teacher-wise/1005.xlsx b/result/templates/teacher-wise/1005.xlsx new file mode 100644 index 0000000..2fa593d Binary files /dev/null and b/result/templates/teacher-wise/1005.xlsx differ diff --git a/result/templates/teacher-wise/1006.xlsx b/result/templates/teacher-wise/1006.xlsx new file mode 100644 index 0000000..77f3498 Binary files /dev/null and b/result/templates/teacher-wise/1006.xlsx differ diff --git a/templates/batch-wise/batch_wise_template.py b/templates/batch-wise/batch_wise_template.py new file mode 100644 index 0000000..f887bcd --- /dev/null +++ b/templates/batch-wise/batch_wise_template.py @@ -0,0 +1,49 @@ +"""Renders one timetable grid per batch.""" + +import sys +import os + +_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..") +sys.path.insert(0, os.path.join(_ROOT, "app")) +sys.path.insert(0, os.path.join(_ROOT, "controller")) + +from xlsx_grid_helper import build_grid_sheet, new_workbook, save_workbook # noqa: E402 +from template_controller import TIME_SLOTS as GRID_TIME_SLOTS # noqa: E402 + + +def _sessions_for_batch(sessions, batch_code): + return [s for s in sessions if batch_code in s["batch_codes"]] + + +def generate_batch_wise_templates(sessions, data, output_dir): + written = [] + for batch in data["batches"]: + batch_code = batch["batch_code"] + batch_sessions = _sessions_for_batch(sessions, batch_code) + + by_day_slot = {} + for s in batch_sessions: + for slot_index in s["slot_indices"]: + by_day_slot[(s["day"], slot_index)] = s + + def cell_text(day, slot_index, by_day_slot=by_day_slot): + s = by_day_slot.get((day, slot_index)) + if not s: + return "" + tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "") + return f'{s["course_name"]}{tag}\n{s["instructor_name"]}\n{s["room_id"]}' + + wb = new_workbook() + ws = wb.active + subtitle = f'{batch["department"]} - {batch["program"]} - Session {batch["session"]}' + build_grid_sheet( + ws, + title=f"Batch {batch_code}", + subtitle=subtitle, + time_slots=GRID_TIME_SLOTS, + sessions_by_day_slot=by_day_slot, + cell_text_fn=cell_text, + ) + path = save_workbook(wb, output_dir, f"{batch_code}.xlsx") + written.append(path) + return written diff --git a/templates/department-wise/department_wise_template.py b/templates/department-wise/department_wise_template.py new file mode 100644 index 0000000..f8d679b --- /dev/null +++ b/templates/department-wise/department_wise_template.py @@ -0,0 +1,54 @@ +"""Renders one timetable grid per department, aggregating all its batches.""" + +import sys +import os + +_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..") +sys.path.insert(0, os.path.join(_ROOT, "app")) +sys.path.insert(0, os.path.join(_ROOT, "controller")) + +from xlsx_grid_helper import build_grid_sheet, new_workbook, save_workbook # noqa: E402 +from template_controller import TIME_SLOTS as GRID_TIME_SLOTS # noqa: E402 + + +def generate_department_wise_templates(sessions, data, output_dir): + departments = sorted({b["department"] for b in data["batches"]}) + written = [] + + for department in departments: + dept_batch_codes = {b["batch_code"] for b in data["batches"] if b["department"] == department} + dept_sessions = [s for s in sessions if any(bc in dept_batch_codes for bc in s["batch_codes"])] + + by_day_slot = {} + for s in dept_sessions: + for slot_index in s["slot_indices"]: + by_day_slot.setdefault((s["day"], slot_index), []).append(s) + + def cell_text(day, slot_index, by_day_slot=by_day_slot): + entries = by_day_slot.get((day, slot_index)) + if not entries: + return "" + lines = [] + for s in entries: + tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "") + batches = "+".join(s["batch_codes"]) + lines.append(f'{s["course_name"]}{tag} [{batches}]\n{s["instructor_name"]} @ {s["room_id"]}') + return "\n---\n".join(lines) + + # for fill-color purposes just use the first session in each cell + first_by_day_slot = {k: v[0] for k, v in by_day_slot.items()} + + wb = new_workbook() + ws = wb.active + build_grid_sheet( + ws, + title=f"Department: {department}", + subtitle="All batches / programs", + time_slots=GRID_TIME_SLOTS, + sessions_by_day_slot=first_by_day_slot, + cell_text_fn=cell_text, + ) + path = save_workbook(wb, output_dir, f"{department}.xlsx") + written.append(path) + + return written diff --git a/templates/room-wise/room_wise_template.py b/templates/room-wise/room_wise_template.py new file mode 100644 index 0000000..8d5832f --- /dev/null +++ b/templates/room-wise/room_wise_template.py @@ -0,0 +1,45 @@ +"""Renders one timetable grid per room.""" + +import sys +import os + +_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..") +sys.path.insert(0, os.path.join(_ROOT, "app")) +sys.path.insert(0, os.path.join(_ROOT, "controller")) + +from xlsx_grid_helper import build_grid_sheet, new_workbook, save_workbook # noqa: E402 +from template_controller import TIME_SLOTS as GRID_TIME_SLOTS # noqa: E402 + + +def generate_room_wise_templates(sessions, data, output_dir): + written = [] + for room in data["rooms"]: + room_id = room["room_id"] + room_sessions = [s for s in sessions if s["room_id"] == room_id] + + by_day_slot = {} + for s in room_sessions: + for slot_index in s["slot_indices"]: + by_day_slot[(s["day"], slot_index)] = s + + def cell_text(day, slot_index, by_day_slot=by_day_slot): + s = by_day_slot.get((day, slot_index)) + if not s: + return "" + tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "") + batches = "+".join(s["batch_codes"]) + return f'{s["course_name"]}{tag}\n{batches}\n{s["instructor_name"]}' + + wb = new_workbook() + ws = wb.active + build_grid_sheet( + ws, + title=f'{room["room_name"]} ({room_id})', + subtitle=f'Block {room["block_id"]} - {room["room_type"].title()} - Capacity {room["capacity"]}', + time_slots=GRID_TIME_SLOTS, + sessions_by_day_slot=by_day_slot, + cell_text_fn=cell_text, + ) + path = save_workbook(wb, output_dir, f"{room_id}.xlsx") + written.append(path) + return written diff --git a/templates/student-wise/student_wise_template.py b/templates/student-wise/student_wise_template.py new file mode 100644 index 0000000..d07014d --- /dev/null +++ b/templates/student-wise/student_wise_template.py @@ -0,0 +1,54 @@ +"""Renders one timetable grid per student (same grid as their batch, labeled per student).""" + +import sys +import os + +_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..") +sys.path.insert(0, os.path.join(_ROOT, "app")) +sys.path.insert(0, os.path.join(_ROOT, "controller")) + +from xlsx_grid_helper import build_grid_sheet, new_workbook, save_workbook # noqa: E402 +from template_controller import TIME_SLOTS as GRID_TIME_SLOTS # noqa: E402 + + +def _sessions_for_batch(sessions, batch_code): + return [s for s in sessions if batch_code in s["batch_codes"]] + + +def generate_student_wise_templates(sessions, data, output_dir): + written = [] + for student in data["students"]: + roll_no = student["roll_no"] + batch_code = student["batch_code"] + batch = data["batches_by_code"].get(batch_code) + student_sessions = _sessions_for_batch(sessions, batch_code) + + by_day_slot = {} + for s in student_sessions: + for slot_index in s["slot_indices"]: + by_day_slot[(s["day"], slot_index)] = s + + def cell_text(day, slot_index, by_day_slot=by_day_slot): + s = by_day_slot.get((day, slot_index)) + if not s: + return "" + tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "") + return f'{s["course_name"]}{tag}\n{s["instructor_name"]}\n{s["room_id"]}' + + subtitle = f'{student["student_name"]} - Batch {batch_code}' + if batch: + subtitle += f' ({batch["department"]} - {batch["program"]})' + + wb = new_workbook() + ws = wb.active + build_grid_sheet( + ws, + title=f"Roll No: {roll_no}", + subtitle=subtitle, + time_slots=GRID_TIME_SLOTS, + sessions_by_day_slot=by_day_slot, + cell_text_fn=cell_text, + ) + path = save_workbook(wb, output_dir, f"{roll_no}.xlsx") + written.append(path) + return written diff --git a/templates/teacher-wise/teacher_wise_template.py b/templates/teacher-wise/teacher_wise_template.py new file mode 100644 index 0000000..46ae62f --- /dev/null +++ b/templates/teacher-wise/teacher_wise_template.py @@ -0,0 +1,45 @@ +"""Renders one timetable grid per instructor.""" + +import sys +import os + +_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..") +sys.path.insert(0, os.path.join(_ROOT, "app")) +sys.path.insert(0, os.path.join(_ROOT, "controller")) + +from xlsx_grid_helper import build_grid_sheet, new_workbook, save_workbook # noqa: E402 +from template_controller import TIME_SLOTS as GRID_TIME_SLOTS # noqa: E402 + + +def generate_teacher_wise_templates(sessions, data, output_dir): + written = [] + for instructor in data["instructors"]: + code = instructor["instructor_code"] + instr_sessions = [s for s in sessions if s["instructor_code"] == code] + + by_day_slot = {} + for s in instr_sessions: + for slot_index in s["slot_indices"]: + by_day_slot[(s["day"], slot_index)] = s + + def cell_text(day, slot_index, by_day_slot=by_day_slot): + s = by_day_slot.get((day, slot_index)) + if not s: + return "" + tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "") + batches = "+".join(s["batch_codes"]) + return f'{s["course_name"]}{tag}\n{batches}\n{s["room_id"]}' + + wb = new_workbook() + ws = wb.active + build_grid_sheet( + ws, + title=f'{instructor["instructor_name"]} ({code})', + subtitle=f'Department: {instructor["department"]}', + time_slots=GRID_TIME_SLOTS, + sessions_by_day_slot=by_day_slot, + cell_text_fn=cell_text, + ) + path = save_workbook(wb, output_dir, f"{code}.xlsx") + written.append(path) + return written