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)}" }