more chnages
This commit is contained in:
@@ -25,6 +25,7 @@ from template_controller import build_schedule # noqa: E402
|
|||||||
from conflict_checker import check_all_conflicts # 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.student_conflict_checker import format_student_conflicts # noqa: E402
|
||||||
from conflict_checker.teacher_conflict_checker import format_teacher_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 batch_wise_template import generate_batch_wise_templates # noqa: E402
|
||||||
from department_wise_template import generate_department_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():
|
def main():
|
||||||
|
reset_log()
|
||||||
|
|
||||||
data_csv_marker = os.path.join(ROOT_DIR, "data", "dummy", "blocks", "rooms.csv")
|
data_csv_marker = os.path.join(ROOT_DIR, "data", "dummy", "blocks", "rooms.csv")
|
||||||
if not os.path.exists(data_csv_marker):
|
if not os.path.exists(data_csv_marker):
|
||||||
print("Dummy data not found — generating it first...")
|
print("Dummy data not found — generating it first...")
|
||||||
@@ -48,19 +51,19 @@ def main():
|
|||||||
data, sessions = build_schedule()
|
data, sessions = build_schedule()
|
||||||
print(f"Scheduled {len(sessions)} sessions.\n")
|
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)
|
report = check_all_conflicts(sessions)
|
||||||
if report["has_conflicts"]:
|
if report["has_conflicts"]:
|
||||||
for line in format_student_conflicts(report["student_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"]):
|
for line in format_teacher_conflicts(report["teacher_conflicts"]):
|
||||||
print(f" TEACHER CONFLICT: {line}")
|
log_error(f"TEACHER CONFLICT: {line}")
|
||||||
print(
|
log_error(
|
||||||
f"Found {len(report['student_conflicts'])} student conflict(s) and "
|
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:
|
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 = [
|
generators = [
|
||||||
("batch-wise", generate_batch_wise_templates),
|
("batch-wise", generate_batch_wise_templates),
|
||||||
|
|||||||
48
app/template_logger.py
Normal file
48
app/template_logger.py
Normal 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)
|
||||||
4
logs/template_generator.log
Normal file
4
logs/template_generator.log
Normal 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.
|
||||||
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.
@@ -21,17 +21,26 @@ def generate_batch_wise_templates(sessions, data, output_dir):
|
|||||||
batch_code = batch["batch_code"]
|
batch_code = batch["batch_code"]
|
||||||
batch_sessions = _sessions_for_batch(sessions, 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 = {}
|
by_day_slot = {}
|
||||||
for s in batch_sessions:
|
for s in batch_sessions:
|
||||||
for slot_index in s["slot_indices"]:
|
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):
|
def cell_text(day, slot_index, by_day_slot=by_day_slot):
|
||||||
s = by_day_slot.get((day, slot_index))
|
entries = by_day_slot.get((day, slot_index))
|
||||||
if not s:
|
if not entries:
|
||||||
return ""
|
return ""
|
||||||
|
lines = []
|
||||||
|
for s in entries:
|
||||||
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
|
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.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()
|
wb = new_workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
@@ -41,7 +50,7 @@ def generate_batch_wise_templates(sessions, data, output_dir):
|
|||||||
title=f"Batch {batch_code}",
|
title=f"Batch {batch_code}",
|
||||||
subtitle=subtitle,
|
subtitle=subtitle,
|
||||||
time_slots=GRID_TIME_SLOTS,
|
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,
|
cell_text_fn=cell_text,
|
||||||
)
|
)
|
||||||
path = save_workbook(wb, output_dir, f"{batch_code}.xlsx")
|
path = save_workbook(wb, output_dir, f"{batch_code}.xlsx")
|
||||||
|
|||||||
@@ -17,18 +17,26 @@ def generate_room_wise_templates(sessions, data, output_dir):
|
|||||||
room_id = room["room_id"]
|
room_id = room["room_id"]
|
||||||
room_sessions = [s for s in sessions if s["room_id"] == 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 = {}
|
by_day_slot = {}
|
||||||
for s in room_sessions:
|
for s in room_sessions:
|
||||||
for slot_index in s["slot_indices"]:
|
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):
|
def cell_text(day, slot_index, by_day_slot=by_day_slot):
|
||||||
s = by_day_slot.get((day, slot_index))
|
entries = by_day_slot.get((day, slot_index))
|
||||||
if not s:
|
if not entries:
|
||||||
return ""
|
return ""
|
||||||
|
lines = []
|
||||||
|
for s in entries:
|
||||||
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
|
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
|
||||||
batches = "+".join(s["batch_codes"])
|
batches = "+".join(s["batch_codes"])
|
||||||
return f'{s["course_name"]}{tag}\n{batches}\n{s["instructor_name"]}'
|
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()
|
wb = new_workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
@@ -37,7 +45,7 @@ def generate_room_wise_templates(sessions, data, output_dir):
|
|||||||
title=f'{room["room_name"]} ({room_id})',
|
title=f'{room["room_name"]} ({room_id})',
|
||||||
subtitle=f'Block {room["block_id"]} - {room["room_type"].title()} - Capacity {room["capacity"]}',
|
subtitle=f'Block {room["block_id"]} - {room["room_type"].title()} - Capacity {room["capacity"]}',
|
||||||
time_slots=GRID_TIME_SLOTS,
|
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,
|
cell_text_fn=cell_text,
|
||||||
)
|
)
|
||||||
path = save_workbook(wb, output_dir, f"{room_id}.xlsx")
|
path = save_workbook(wb, output_dir, f"{room_id}.xlsx")
|
||||||
|
|||||||
@@ -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 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_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):
|
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)
|
batch = data["batches_by_code"].get(batch_code)
|
||||||
student_sessions = _sessions_for_batch(sessions, 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 s in student_sessions:
|
||||||
for slot_index in s["slot_indices"]:
|
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):
|
by_day_slot = {}
|
||||||
s = by_day_slot.get((day, slot_index))
|
for (day, slot_index), entries in raw_by_day_slot.items():
|
||||||
if not s:
|
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 ""
|
return ""
|
||||||
|
s = entries[0]
|
||||||
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
|
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}'
|
subtitle = f'{student["student_name"]} - Batch {batch_code}'
|
||||||
if batch:
|
if batch:
|
||||||
|
|||||||
@@ -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 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_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):
|
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"]
|
code = instructor["instructor_code"]
|
||||||
instr_sessions = [s for s in sessions if s["instructor_code"] == 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 s in instr_sessions:
|
||||||
for slot_index in s["slot_indices"]:
|
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):
|
by_day_slot = {}
|
||||||
s = by_day_slot.get((day, slot_index))
|
for (day, slot_index), entries in raw_by_day_slot.items():
|
||||||
if not s:
|
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 ""
|
return ""
|
||||||
|
s = entries[0]
|
||||||
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
|
tag = " (Lab)" if s["course_type"] == "lab" else (" (General)" if s["is_general"] else "")
|
||||||
batches = "+".join(s["batch_codes"])
|
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()
|
wb = new_workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
|
|||||||
Reference in New Issue
Block a user