more chnages

This commit is contained in:
Talhadeveloperr
2026-09-09 17:07:11 +05:00
parent a7bb2f3dae
commit 1c5b8f38c8
57 changed files with 147 additions and 31 deletions

View File

@@ -25,6 +25,7 @@ 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 template_logger import reset_log, log_error, log_info # noqa: E402
from batch_wise_template import generate_batch_wise_templates # noqa: E402
from department_wise_template import generate_department_wise_templates # noqa: E402
@@ -36,6 +37,8 @@ RESULT_DIR = os.path.join(ROOT_DIR, "result", "templates")
def main():
reset_log()
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...")
@@ -48,19 +51,19 @@ def main():
data, sessions = build_schedule()
print(f"Scheduled {len(sessions)} sessions.\n")
print("Running conflict checks (student/batch and teacher)...")
log_info("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}")
log_error(f"STUDENT CONFLICT: {line}")
for line in format_teacher_conflicts(report["teacher_conflicts"]):
print(f" TEACHER CONFLICT: {line}")
print(
log_error(f"TEACHER CONFLICT: {line}")
log_error(
f"Found {len(report['student_conflicts'])} student conflict(s) and "
f"{len(report['teacher_conflicts'])} teacher conflict(s).\n"
f"{len(report['teacher_conflicts'])} teacher conflict(s)."
)
else:
print("No conflicts found — schedule is clash-free for students and teachers.\n")
log_info("No conflicts found — schedule is clash-free for students and teachers.")
generators = [
("batch-wise", generate_batch_wise_templates),

48
app/template_logger.py Normal file
View File

@@ -0,0 +1,48 @@
"""
Simple run-scoped logger for the template generator.
The log file under logs/ is erased and recreated at the start of every run
(see reset_log()) so it only ever reflects the most recent generation.
"""
import datetime
import os
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOGS_DIR = os.path.join(ROOT_DIR, "logs")
LOG_FILE = os.path.join(LOGS_DIR, "template_generator.log")
_log_path = None
def reset_log():
"""Erases any previous log and starts a fresh one for this run."""
global _log_path
os.makedirs(LOGS_DIR, exist_ok=True)
_log_path = LOG_FILE
with open(_log_path, "w", encoding="utf-8") as f:
f.write(f"Timetable generation log - started {datetime.datetime.now().isoformat()}\n")
f.write("=" * 80 + "\n")
return _log_path
def log_error(message):
"""Appends an ERROR line to the current run's log file and prints it."""
_write("ERROR", message)
def log_warning(message):
_write("WARNING", message)
def log_info(message):
_write("INFO", message)
def _write(level, message):
if _log_path is None:
reset_log()
line = f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {level}: {message}"
with open(_log_path, "a", encoding="utf-8") as f:
f.write(line + "\n")
print(line)

View File

@@ -0,0 +1,4 @@
Timetable generation log - started 2026-09-09T17:06:29.991568
================================================================================
[17:06:29] INFO: Running conflict checks (student/batch and teacher)...
[17:06:29] INFO: No conflicts found — schedule is clash-free for students and teachers.

View File

@@ -21,17 +21,26 @@ def generate_batch_wise_templates(sessions, data, output_dir):
batch_code = batch["batch_code"]
batch_sessions = _sessions_for_batch(sessions, batch_code)
# A batch-wise view can legitimately show more than one lecture in
# the same slot (e.g. sections of the batch split across rooms), so
# every session landing on a cell is kept and rendered — none are
# dropped or overwritten.
by_day_slot = {}
for s in batch_sessions:
for slot_index in s["slot_indices"]:
by_day_slot[(s["day"], slot_index)] = s
by_day_slot.setdefault((s["day"], slot_index), []).append(s)
def cell_text(day, slot_index, by_day_slot=by_day_slot):
s = by_day_slot.get((day, slot_index))
if not s:
entries = by_day_slot.get((day, slot_index))
if not entries:
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"]}'
lines = []
for s in entries:
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
lines.append(f'{s["course_name"]}{tag}\n{s["instructor_name"]}\n{s["room_id"]}')
return "\n---\n".join(lines)
first_by_day_slot = {k: v[0] for k, v in by_day_slot.items()}
wb = new_workbook()
ws = wb.active
@@ -41,7 +50,7 @@ def generate_batch_wise_templates(sessions, data, output_dir):
title=f"Batch {batch_code}",
subtitle=subtitle,
time_slots=GRID_TIME_SLOTS,
sessions_by_day_slot=by_day_slot,
sessions_by_day_slot=first_by_day_slot,
cell_text_fn=cell_text,
)
path = save_workbook(wb, output_dir, f"{batch_code}.xlsx")

View File

@@ -17,18 +17,26 @@ def generate_room_wise_templates(sessions, data, output_dir):
room_id = room["room_id"]
room_sessions = [s for s in sessions if s["room_id"] == room_id]
# A room-wise view can legitimately show more than one lecture in
# the same slot (e.g. a large room split/shared), so every session
# landing on a cell is kept and rendered — none are dropped.
by_day_slot = {}
for s in room_sessions:
for slot_index in s["slot_indices"]:
by_day_slot[(s["day"], slot_index)] = s
by_day_slot.setdefault((s["day"], slot_index), []).append(s)
def cell_text(day, slot_index, by_day_slot=by_day_slot):
s = by_day_slot.get((day, slot_index))
if not s:
entries = by_day_slot.get((day, slot_index))
if not entries:
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"]}'
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}\n{batches}\n{s["instructor_name"]}')
return "\n---\n".join(lines)
first_by_day_slot = {k: v[0] for k, v in by_day_slot.items()}
wb = new_workbook()
ws = wb.active
@@ -37,7 +45,7 @@ def generate_room_wise_templates(sessions, data, output_dir):
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,
sessions_by_day_slot=first_by_day_slot,
cell_text_fn=cell_text,
)
path = save_workbook(wb, output_dir, f"{room_id}.xlsx")

View File

@@ -9,6 +9,7 @@ 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
from template_logger import log_error # noqa: E402
def _sessions_for_batch(sessions, batch_code):
@@ -23,17 +24,38 @@ def generate_student_wise_templates(sessions, data, output_dir):
batch = data["batches_by_code"].get(batch_code)
student_sessions = _sessions_for_batch(sessions, batch_code)
by_day_slot = {}
# A student can only be in one place at a time: each (day, slot)
# must resolve to exactly one session. If the schedule places more
# than one, that's a real scheduling error - log it instead of
# silently picking/overwriting one.
raw_by_day_slot = {}
for s in student_sessions:
for slot_index in s["slot_indices"]:
by_day_slot[(s["day"], slot_index)] = s
raw_by_day_slot.setdefault((s["day"], slot_index), []).append(s)
def cell_text(day, slot_index, by_day_slot=by_day_slot):
s = by_day_slot.get((day, slot_index))
if not s:
by_day_slot = {}
for (day, slot_index), entries in raw_by_day_slot.items():
if len(entries) > 1:
course_list = ", ".join(
f'{e["course_id"]} ({e["course_type"]}) with {e["instructor_name"]} in {e["room_id"]}'
for e in entries
)
log_error(
f"Student conflict: roll {roll_no} (batch {batch_code}) has "
f"{len(entries)} overlapping courses on {day} slot {slot_index}: {course_list}"
)
by_day_slot[(day, slot_index)] = entries[0]
def cell_text(day, slot_index, raw_by_day_slot=raw_by_day_slot):
entries = raw_by_day_slot.get((day, slot_index))
if not entries:
return ""
s = entries[0]
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"]}'
text = f'{s["course_name"]}{tag}\n{s["instructor_name"]}\n{s["room_id"]}'
if len(entries) > 1:
text = "CONFLICT!\n" + text
return text
subtitle = f'{student["student_name"]} - Batch {batch_code}'
if batch:

View File

@@ -9,6 +9,7 @@ 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
from template_logger import log_error # noqa: E402
def generate_teacher_wise_templates(sessions, data, output_dir):
@@ -17,18 +18,39 @@ def generate_teacher_wise_templates(sessions, data, output_dir):
code = instructor["instructor_code"]
instr_sessions = [s for s in sessions if s["instructor_code"] == code]
by_day_slot = {}
# A teacher can only be in one place at a time: each (day, slot)
# must resolve to exactly one session. If the schedule places more
# than one, that's a real scheduling error - log it instead of
# silently picking/overwriting one.
raw_by_day_slot = {}
for s in instr_sessions:
for slot_index in s["slot_indices"]:
by_day_slot[(s["day"], slot_index)] = s
raw_by_day_slot.setdefault((s["day"], slot_index), []).append(s)
def cell_text(day, slot_index, by_day_slot=by_day_slot):
s = by_day_slot.get((day, slot_index))
if not s:
by_day_slot = {}
for (day, slot_index), entries in raw_by_day_slot.items():
if len(entries) > 1:
course_list = ", ".join(
f'{e["course_id"]} ({e["course_type"]}) for batch(es) {"+".join(e["batch_codes"])} in {e["room_id"]}'
for e in entries
)
log_error(
f"Teacher conflict: {instructor['instructor_name']} ({code}) is double-booked on "
f"{day} slot {slot_index}: {course_list}"
)
by_day_slot[(day, slot_index)] = entries[0]
def cell_text(day, slot_index, raw_by_day_slot=raw_by_day_slot):
entries = raw_by_day_slot.get((day, slot_index))
if not entries:
return ""
s = entries[0]
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"]}'
text = f'{s["course_name"]}{tag}\n{batches}\n{s["room_id"]}'
if len(entries) > 1:
text = "CONFLICT!\n" + text
return text
wb = new_workbook()
ws = wb.active