api/modules/database/init/init_user_timetable.py
2025-11-14 14:47:19 +00:00

326 lines
15 KiB
Python

import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
import modules.database.tools.neo4j_driver_tools as driver
import modules.database.tools.neontology_tools as neon
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
from modules.database.schemas.nodes.users import UserNode
from modules.database.schemas.nodes.schools.schools import SubjectClassNode
from modules.database.schemas.nodes.workers.workers import TeacherNode,
from modules.database.schemas.nodes.calendars import CalendarDayNode
from modules.database.schemas.nodes.workers.timetable import (
UserTeacherTimetableNode, TimetableLessonNode
)
from modules.database.schemas.relationships.entity_timetable_rels import (
EntityHasTimetable
)
from modules.database.schemas.relationships.planning_relationships import (
TeacherHasTimetable, TimetableHasClass, ClassHasLesson,TimetableLessonFollowsTimetableLesson
)
from modules.database.schemas.relationships.calendar_timetable_rels import (
CalendarDayHasTimetableLesson, TimetableLessonBelongsToCalendarDay,
CalendarDayHasPlannedLesson, PlannedLessonBelongsToCalendarDay
)
def get_school_worker_classes(school_db_name: str, user_uuid_string: str, worker_uuid_string: str) -> list:
"""
Retrieve all classes for a worker from the school database.
"""
query = """
MATCH (w:Teacher {uuid_string: $worker_id})-[:TEACHER_HAS_TIMETABLE]->(tt:TeacherTimetable)
-[:TIMETABLE_HAS_CLASS]->(c:SubjectClass)
RETURN c
"""
with driver.get_driver(db_name=school_db_name).session(database=school_db_name) as session:
result = session.run(query, worker_id=worker_uuid_string)
classes = [record['c'] for record in result]
if not classes:
logger.warning(f"No classes found for teacher {worker_uuid_string} in school database")
return classes
def get_school_class_periods(school_db_name: str, class_uuid_string: str) -> list:
"""
Retrieve all periods for a class from the school database.
"""
query = """
MATCH (c:SubjectClass {uuid_string: $class_id})-[:CLASS_HAS_LESSON]->(l:TimetableLesson)
RETURN l
"""
with driver.get_driver(db_name=school_db_name).session(database=school_db_name) as session:
result = session.run(query, class_id=class_uuid_string)
periods = [record['l'] for record in result]
if not periods:
logger.warning(f"No periods found for class {class_uuid_string} in school database")
return periods
def get_user_calendar_nodes(user_db_name: str, user_node: UserNode) -> list:
"""
Retrieve all calendar day nodes for a user.
"""
# First try to find any calendar days to verify the structure
verify_query = """
MATCH (w:User {uuid_string: $user_id})
OPTIONAL MATCH (w)-[:HAS_CALENDAR]->(c:Calendar)
OPTIONAL MATCH (c)-[:CALENDAR_INCLUDES_YEAR]->(y:CalendarYear)
OPTIONAL MATCH (y)-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth)
OPTIONAL MATCH (m)-[:MONTH_INCLUDES_DAY]->(d:CalendarDay)
RETURN w.uuid_string as user_id,
count(c) as calendar_count,
count(y) as year_count,
count(m) as month_count,
count(d) as day_count,
collect(DISTINCT y.year) as years
LIMIT 1
"""
with driver.get_driver(db_name=user_db_name).session(database=user_db_name) as session:
# First check the calendar structure
result = session.run(verify_query, user_id=user_node.uuid_string)
if stats := result.single():
logger.info(f"Calendar structure for user {stats['user_id']}: "
f"calendars={stats['calendar_count']}, "
f"years={stats['year_count']}, "
f"months={stats['month_count']}, "
f"days={stats['day_count']}, "
f"available years={stats['years']}")
if stats['calendar_count'] == 0:
logger.error(f"No calendar found for user {user_node.uuid_string}")
return []
if stats['year_count'] == 0:
logger.error(f"No calendar years found for user {user_node.uuid_string}")
return []
if stats['month_count'] == 0:
logger.error(f"No calendar months found for user {user_node.uuid_string}")
return []
if stats['day_count'] == 0:
logger.error(f"No calendar days found for user {user_node.uuid_string}")
return []
# Get all calendar days without year filter
query = """
MATCH (w:User {uuid_string: $user_id})-[:HAS_CALENDAR]->(c:Calendar)
-[:CALENDAR_INCLUDES_YEAR]->(y:CalendarYear)
-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth)
-[:MONTH_INCLUDES_DAY]->(d:CalendarDay)
RETURN d.uuid_string as uuid_string,
d.date as date,
d.day_of_week as day_of_week,
d.iso_day as iso_day,
d.node_storage_path as path
ORDER BY d.date
"""
result = session.run(query, user_id=user_node.uuid_string)
calendar_days = []
for record in result:
calendar_day = CalendarDayNode(
uuid_string=record['uuid_string'],
date=record['date'],
day_of_week=record['day_of_week'],
iso_day=record['iso_day'],
node_storage_path=record['path']
)
calendar_days.append(calendar_day)
if not calendar_days:
logger.error(f"No calendar days found for user {user_node.uuid_string}")
else:
# Log the date range we have
dates = sorted([day.date for day in calendar_days])
logger.info(f"Found {len(calendar_days)} calendar days for user {user_node.uuid_string}")
logger.info(f"Calendar days range from {dates[0]} to {dates[-1]}")
return calendar_days
def create_user_worker_timetable(
user_node: UserNode,
user_worker_node: TeacherNode,
school_db_name: str
):
"""
Create a worker timetable structure in the user's database that mirrors
the school timetable, with lessons linked to the user's calendar structure.
"""
user_db_name = user_worker_node.user_db_name
# Initialize filesystem and Neo4j
fs_handler = ClassroomCopilotFilesystem(db_name=user_db_name, init_run_type="user")
# Create teacher timetable directory under the worker's directory
_, worker_timetable_path = fs_handler.create_teacher_timetable_directory(user_worker_node.node_storage_path)
# Initialize neontology connection
neon.init_neontology_connection()
# Get user's calendar nodes
calendar_nodes = get_user_calendar_nodes(user_db_name, user_node)
if not calendar_nodes:
logger.warning(f"No calendar nodes found for user {user_node.uuid_string}")
return {
"status": "error",
"message": "No calendar nodes found for user"
}
try:
# Create UserTeacherTimetableNode
timetable_uuid_string = f"UserTeacherTimetable_{user_worker_node.teacher_code}"
worker_timetable = UserTeacherTimetableNode(
uuid_string=timetable_uuid_string,
school_db_name=school_db_name,
school_timetable_id=f"TeacherTimetable_{user_worker_node.teacher_code}",
node_storage_path=worker_timetable_path
)
# Create the timetable node and its tldraw file
neon.create_or_merge_neontology_node(worker_timetable, database=user_db_name, operation='merge')
fs_handler.create_default_tldraw_file(worker_timetable.node_storage_path, worker_timetable.to_dict())
# Link timetable to teacher using the correct relationship structure
neon.create_or_merge_neontology_relationship(
TeacherHasTimetable(source=user_worker_node, target=worker_timetable),
database=user_db_name,
operation='merge'
)
# Get classes from school database
school_classes = get_school_worker_classes(school_db_name, user_node.uuid_string, user_worker_node.uuid_string)
if not school_classes:
logger.warning(f"No classes found for teacher {user_worker_node.uuid_string} in school database")
return {
"status": "warning",
"message": "No classes found in school database"
}
# Dictionary to store lessons by class
class_lessons = {}
for class_data in school_classes:
class_name_safe = class_data['subject_class_code'].replace(' ', '_')
_, class_path = fs_handler.create_teacher_class_directory(worker_timetable_path, class_name_safe)
# Create SubjectClassNode
subject_class_node = SubjectClassNode(
uuid_string=class_data['uuid_string'],
subject_class_code=class_data['subject_class_code'],
year_group=class_data['year_group'],
subject=class_data['subject'],
subject_code=class_data['subject_code'],
node_storage_path=class_path
)
neon.create_or_merge_neontology_node(subject_class_node, database=user_db_name, operation='merge')
fs_handler.create_default_tldraw_file(subject_class_node.node_storage_path, subject_class_node.to_dict())
# Link class to timetable
neon.create_or_merge_neontology_relationship(
TimetableHasClass(source=worker_timetable, target=subject_class_node),
database=user_db_name,
operation='merge'
)
# Initialize empty list for this class's lessons
class_lessons[class_data['uuid_string']] = []
# Get periods from school database
periods = get_school_class_periods(school_db_name, class_data['uuid_string'])
if not periods:
logger.warning(f"No periods found for class {class_data['uuid_string']} in school database")
continue
for period_data in periods:
# Create TimetableLessonNode
lesson_uuid_string = f"UserTimetableLesson_{timetable_uuid_string}_{class_name_safe}_{period_data['date']}_{period_data['period_code']}"
timetable_lesson_node = TimetableLessonNode(
uuid_string=lesson_uuid_string,
subject_class=class_data['subject_class_code'],
date=period_data['date'],
start_time=period_data['start_time'],
end_time=period_data['end_time'],
period_code=period_data['period_code'],
school_db_name=school_db_name,
school_period_id=period_data['uuid_string'],
node_storage_path="Not set" # Will be set after creating directories
)
if calendar_day := next(
(
day
for day in calendar_nodes
if day.date == period_data['date']
),
None,
):
# Create lesson directory using calendar info
_, lesson_path = fs_handler.create_teacher_timetable_lesson_directory(
class_path,
f"{calendar_day.date}_{period_data['period_code']}"
)
timetable_lesson_node.node_storage_path = lesson_path
# Create and link nodes
neon.create_or_merge_neontology_node(timetable_lesson_node, database=user_db_name, operation='merge')
fs_handler.create_default_tldraw_file(timetable_lesson_node.node_storage_path, timetable_lesson_node.to_dict())
# Link lesson to class
neon.create_or_merge_neontology_relationship(
ClassHasLesson(source=subject_class_node, target=timetable_lesson_node),
database=user_db_name,
operation='merge'
)
# Link lesson to calendar day (keeping only one direction)
neon.create_or_merge_neontology_relationship(
CalendarDayHasTimetableLesson(
source=calendar_day,
target=timetable_lesson_node
),
database=user_db_name,
operation='merge'
)
# Store the lesson node
class_lessons[class_data['uuid_string']].append({
'node': timetable_lesson_node,
'date': period_data['date'],
'start_time': period_data['start_time']
})
else:
logger.warning(f"No calendar day found for date {period_data['date']} - this is expected if the date is not in the current calendar year")
# Create sequential relationships for each class
for class_id, lessons in class_lessons.items():
# Sort lessons by date and start time
sorted_lessons = sorted(lessons, key=lambda x: (x['date'], x['start_time']))
# Create relationships between consecutive lessons
for i in range(len(sorted_lessons) - 1):
current_lesson = sorted_lessons[i]['node']
next_lesson = sorted_lessons[i + 1]['node']
# Skip if current and next lesson are the same node
if current_lesson.uuid_string != next_lesson.uuid_string:
neon.create_or_merge_neontology_relationship(
TimetableLessonFollowsTimetableLesson(
source=current_lesson,
target=next_lesson
),
database=user_db_name,
operation='merge'
)
logger.info(f"Created sequential relationships for class {class_id}")
logger.info(f"Successfully created user timetable structure for {user_worker_node.teacher_code}")
return {
"status": "success",
"message": "User timetable structure created successfully",
"timetable_node": worker_timetable.to_dict()
}
except Exception as e:
logger.error(f"Error creating user timetable structure: {str(e)}")
return {
"status": "error",
"message": f"Error creating user timetable structure: {str(e)}"
}