initial project

This commit is contained in:
Talhadeveloperr
2026-09-09 17:00:54 +05:00
commit a7bb2f3dae
72 changed files with 1051 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.pyc

84
app/template_generator.py Normal file
View 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
View 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

View 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),
}

View 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

View 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

View 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

View File

@@ -0,0 +1,4 @@
batch_code,department,program,session
25021519,CS,BSCS,25
25021520,CS,BSSE,25
25031519,EE,BSEE,25
1 batch_code department program session
2 25021519 CS BSCS 25
3 25021520 CS BSSE 25
4 25031519 EE BSEE 25

View File

@@ -0,0 +1,4 @@
batch_code,department,program,session
26021519,CS,BSCS,26
26021520,CS,BSSE,26
26031519,EE,BSEE,26
1 batch_code department program session
2 26021519 CS BSCS 26
3 26021520 CS BSSE 26
4 26031519 EE BSEE 26

View File

@@ -0,0 +1,2 @@
block_id,block_name
A,Block A
1 block_id block_name
2 A Block A

View 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
1 room_id room_name block_id room_type capacity
2 A-101 Room 101 A classroom 60
3 A-102 Room 102 A classroom 60
4 A-103 Room 103 A classroom 50
5 A-201 Room 201 A classroom 60
6 A-L01 Computer Lab 1 A lab 35
7 A-L02 Computer Lab 2 A lab 35

View 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
1 course_id batch_code instructor_code
2 CS101 25021519 1001
3 CS102 25021519 1001
4 CS201 25021519 1002
5 CS202 25021519 1002
6 CS101 25021520 1001
7 CS102 25021520 1001
8 CS201 25021520 1002
9 CS202 25021520 1002
10 EE101 25031519 1004
11 EE102 25031519 1004
12 CS101 26021519 1001
13 CS102 26021519 1001
14 CS201 26021519 1002
15 CS202 26021519 1002
16 CS101 26021520 1001
17 CS102 26021520 1001
18 CS201 26021520 1002
19 CS202 26021520 1002
20 EE101 26031519 1004
21 EE102 26031519 1004
22 GEN101 25021519|25021520|25031519 1002
23 GEN102 25021519|25021520|25031519 1006
24 GEN101 26021519|26021520|26031519 1002
25 GEN102 26021519|26021520|26031519 1006

View 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
1 course_id course_name course_type credit_hours is_general
2 CS101 Programming Fundamentals theory 3 no
3 CS102 Programming Fundamentals Lab lab 1 no
4 CS201 Data Structures theory 3 no
5 CS202 Data Structures Lab lab 1 no
6 EE101 Basic Electronics theory 3 no
7 EE102 Basic Electronics Lab lab 1 no
8 GEN101 Functional English theory 3 yes
9 GEN102 Islamic Studies theory 2 yes

View 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()

View 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
1 instructor_code instructor_name department
2 1001 Dr. Ahsan Raza CS
3 1002 Ms. Sana Tariq CS
4 1003 Mr. Bilal Khan CS
5 1004 Dr. Farah Iqbal EE
6 1005 Mr. Usman Ali EE
7 1006 Ms. Hira Shah CS

View 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 roll_no batch_code student_name
2 25021519-001 25021519 Student 25021519-001
3 25021519-002 25021519 Student 25021519-002
4 25021519-003 25021519 Student 25021519-003
5 25021519-004 25021519 Student 25021519-004
6 25021519-005 25021519 Student 25021519-005
7 25021520-001 25021520 Student 25021520-001
8 25021520-002 25021520 Student 25021520-002
9 25021520-003 25021520 Student 25021520-003
10 25021520-004 25021520 Student 25021520-004
11 25021520-005 25021520 Student 25021520-005
12 25031519-001 25031519 Student 25031519-001
13 25031519-002 25031519 Student 25031519-002
14 25031519-003 25031519 Student 25031519-003
15 25031519-004 25031519 Student 25031519-004
16 25031519-005 25031519 Student 25031519-005
17 26021519-001 26021519 Student 26021519-001
18 26021519-002 26021519 Student 26021519-002
19 26021519-003 26021519 Student 26021519-003
20 26021519-004 26021519 Student 26021519-004
21 26021519-005 26021519 Student 26021519-005
22 26021520-001 26021520 Student 26021520-001
23 26021520-002 26021520 Student 26021520-002
24 26021520-003 26021520 Student 26021520-003
25 26021520-004 26021520 Student 26021520-004
26 26021520-005 26021520 Student 26021520-005
27 26031519-001 26031519 Student 26031519-001
28 26031519-002 26031519 Student 26031519-002
29 26031519-003 26031519 Student 26031519-003
30 26031519-004 26031519 Student 26031519-004
31 26031519-005 26031519 Student 26031519-005

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
openpyxl>=3.1

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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

View 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

View 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