initial project
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.pyc
|
||||||
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
|
||||||
19
controller/conflict_checker/__init__.py
Normal file
19
controller/conflict_checker/__init__.py
Normal file
@@ -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),
|
||||||
|
}
|
||||||
63
controller/conflict_checker/student_conflict_checker.py
Normal file
63
controller/conflict_checker/student_conflict_checker.py
Normal file
@@ -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
|
||||||
59
controller/conflict_checker/teacher_conflict_checker.py
Normal file
59
controller/conflict_checker/teacher_conflict_checker.py
Normal file
@@ -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
|
||||||
206
controller/template_controller.py
Normal file
206
controller/template_controller.py
Normal file
@@ -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
|
||||||
4
data/dummy/batch_codes/25/batches.csv
Normal file
4
data/dummy/batch_codes/25/batches.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
batch_code,department,program,session
|
||||||
|
25021519,CS,BSCS,25
|
||||||
|
25021520,CS,BSSE,25
|
||||||
|
25031519,EE,BSEE,25
|
||||||
|
4
data/dummy/batch_codes/26/batches.csv
Normal file
4
data/dummy/batch_codes/26/batches.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
batch_code,department,program,session
|
||||||
|
26021519,CS,BSCS,26
|
||||||
|
26021520,CS,BSSE,26
|
||||||
|
26031519,EE,BSEE,26
|
||||||
|
2
data/dummy/blocks/blocks.csv
Normal file
2
data/dummy/blocks/blocks.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
block_id,block_name
|
||||||
|
A,Block A
|
||||||
|
7
data/dummy/blocks/rooms.csv
Normal file
7
data/dummy/blocks/rooms.csv
Normal file
@@ -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
|
||||||
|
25
data/dummy/courses/course_offering.csv
Normal file
25
data/dummy/courses/course_offering.csv
Normal file
@@ -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
|
||||||
|
9
data/dummy/courses/courses.csv
Normal file
9
data/dummy/courses/courses.csv
Normal file
@@ -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
|
||||||
|
160
data/dummy/generate_dummy_data.py
Normal file
160
data/dummy/generate_dummy_data.py
Normal file
@@ -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()
|
||||||
7
data/dummy/instructor/instructors.csv
Normal file
7
data/dummy/instructor/instructors.csv
Normal file
@@ -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
|
||||||
|
31
data/dummy/roll_no/students.csv
Normal file
31
data/dummy/roll_no/students.csv
Normal file
@@ -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
|
||||||
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
openpyxl>=3.1
|
||||||
BIN
result/templates/batch-wise/25021519.xlsx
Normal file
BIN
result/templates/batch-wise/25021519.xlsx
Normal file
Binary file not shown.
BIN
result/templates/batch-wise/25021520.xlsx
Normal file
BIN
result/templates/batch-wise/25021520.xlsx
Normal file
Binary file not shown.
BIN
result/templates/batch-wise/25031519.xlsx
Normal file
BIN
result/templates/batch-wise/25031519.xlsx
Normal file
Binary file not shown.
BIN
result/templates/batch-wise/26021519.xlsx
Normal file
BIN
result/templates/batch-wise/26021519.xlsx
Normal file
Binary file not shown.
BIN
result/templates/batch-wise/26021520.xlsx
Normal file
BIN
result/templates/batch-wise/26021520.xlsx
Normal file
Binary file not shown.
BIN
result/templates/batch-wise/26031519.xlsx
Normal file
BIN
result/templates/batch-wise/26031519.xlsx
Normal file
Binary file not shown.
BIN
result/templates/department-wise/CS.xlsx
Normal file
BIN
result/templates/department-wise/CS.xlsx
Normal file
Binary file not shown.
BIN
result/templates/department-wise/EE.xlsx
Normal file
BIN
result/templates/department-wise/EE.xlsx
Normal file
Binary file not shown.
BIN
result/templates/room-wise/A-101.xlsx
Normal file
BIN
result/templates/room-wise/A-101.xlsx
Normal file
Binary file not shown.
BIN
result/templates/room-wise/A-102.xlsx
Normal file
BIN
result/templates/room-wise/A-102.xlsx
Normal file
Binary file not shown.
BIN
result/templates/room-wise/A-103.xlsx
Normal file
BIN
result/templates/room-wise/A-103.xlsx
Normal file
Binary file not shown.
BIN
result/templates/room-wise/A-201.xlsx
Normal file
BIN
result/templates/room-wise/A-201.xlsx
Normal file
Binary file not shown.
BIN
result/templates/room-wise/A-L01.xlsx
Normal file
BIN
result/templates/room-wise/A-L01.xlsx
Normal file
Binary file not shown.
BIN
result/templates/room-wise/A-L02.xlsx
Normal file
BIN
result/templates/room-wise/A-L02.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021519-001.xlsx
Normal file
BIN
result/templates/student-wise/25021519-001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021519-002.xlsx
Normal file
BIN
result/templates/student-wise/25021519-002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021519-003.xlsx
Normal file
BIN
result/templates/student-wise/25021519-003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021519-004.xlsx
Normal file
BIN
result/templates/student-wise/25021519-004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021519-005.xlsx
Normal file
BIN
result/templates/student-wise/25021519-005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021520-001.xlsx
Normal file
BIN
result/templates/student-wise/25021520-001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021520-002.xlsx
Normal file
BIN
result/templates/student-wise/25021520-002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021520-003.xlsx
Normal file
BIN
result/templates/student-wise/25021520-003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021520-004.xlsx
Normal file
BIN
result/templates/student-wise/25021520-004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25021520-005.xlsx
Normal file
BIN
result/templates/student-wise/25021520-005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25031519-001.xlsx
Normal file
BIN
result/templates/student-wise/25031519-001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25031519-002.xlsx
Normal file
BIN
result/templates/student-wise/25031519-002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25031519-003.xlsx
Normal file
BIN
result/templates/student-wise/25031519-003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25031519-004.xlsx
Normal file
BIN
result/templates/student-wise/25031519-004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/25031519-005.xlsx
Normal file
BIN
result/templates/student-wise/25031519-005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021519-001.xlsx
Normal file
BIN
result/templates/student-wise/26021519-001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021519-002.xlsx
Normal file
BIN
result/templates/student-wise/26021519-002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021519-003.xlsx
Normal file
BIN
result/templates/student-wise/26021519-003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021519-004.xlsx
Normal file
BIN
result/templates/student-wise/26021519-004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021519-005.xlsx
Normal file
BIN
result/templates/student-wise/26021519-005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021520-001.xlsx
Normal file
BIN
result/templates/student-wise/26021520-001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021520-002.xlsx
Normal file
BIN
result/templates/student-wise/26021520-002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021520-003.xlsx
Normal file
BIN
result/templates/student-wise/26021520-003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021520-004.xlsx
Normal file
BIN
result/templates/student-wise/26021520-004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26021520-005.xlsx
Normal file
BIN
result/templates/student-wise/26021520-005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26031519-001.xlsx
Normal file
BIN
result/templates/student-wise/26031519-001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26031519-002.xlsx
Normal file
BIN
result/templates/student-wise/26031519-002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26031519-003.xlsx
Normal file
BIN
result/templates/student-wise/26031519-003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26031519-004.xlsx
Normal file
BIN
result/templates/student-wise/26031519-004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/student-wise/26031519-005.xlsx
Normal file
BIN
result/templates/student-wise/26031519-005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/teacher-wise/1001.xlsx
Normal file
BIN
result/templates/teacher-wise/1001.xlsx
Normal file
Binary file not shown.
BIN
result/templates/teacher-wise/1002.xlsx
Normal file
BIN
result/templates/teacher-wise/1002.xlsx
Normal file
Binary file not shown.
BIN
result/templates/teacher-wise/1003.xlsx
Normal file
BIN
result/templates/teacher-wise/1003.xlsx
Normal file
Binary file not shown.
BIN
result/templates/teacher-wise/1004.xlsx
Normal file
BIN
result/templates/teacher-wise/1004.xlsx
Normal file
Binary file not shown.
BIN
result/templates/teacher-wise/1005.xlsx
Normal file
BIN
result/templates/teacher-wise/1005.xlsx
Normal file
Binary file not shown.
BIN
result/templates/teacher-wise/1006.xlsx
Normal file
BIN
result/templates/teacher-wise/1006.xlsx
Normal file
Binary file not shown.
49
templates/batch-wise/batch_wise_template.py
Normal file
49
templates/batch-wise/batch_wise_template.py
Normal file
@@ -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
|
||||||
54
templates/department-wise/department_wise_template.py
Normal file
54
templates/department-wise/department_wise_template.py
Normal file
@@ -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
|
||||||
45
templates/room-wise/room_wise_template.py
Normal file
45
templates/room-wise/room_wise_template.py
Normal file
@@ -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
|
||||||
54
templates/student-wise/student_wise_template.py
Normal file
54
templates/student-wise/student_wise_template.py
Normal file
@@ -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
|
||||||
45
templates/teacher-wise/teacher_wise_template.py
Normal file
45
templates/teacher-wise/teacher_wise_template.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user