import os from modules.logger_tool import initialise_logger logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) from datetime import timedelta, datetime import pandas as pd from modules.database.schemas.structures import structures import modules.database.schemas.nodes.schools.timetable as timetable import modules.database.schemas.relationships.timetables as tt_rels import modules.database.schemas.relationships.entity_timetable_rels as entity_tt_rels import modules.database.schemas.relationships.calendar_timetable_rels as cal_tt_rels import modules.database.init.init_calendar as init_calendar import modules.database.tools.neontology_tools as neon def create_school_timetable(dataframes, db_name, school_node=None): logger.info(f"Creating school timetable for {db_name}") if dataframes is None: raise ValueError("Data is required to create the calendar and timetable.") logger.info(f"Initialising neo4j connection...") neon.init_neontology_connection() school_df = dataframes['school'] if school_node is None: logger.info(f"School node is None, using school data from dataframe") school_unique_id = school_df[school_df['Identifier'] == 'SchoolID']['Data'].iloc[0] else: logger.info(f"School node is not None, using school data from school node: {school_node}") school_unique_id = school_node.unique_id terms_df = dataframes['terms'] weeks_df = dataframes['weeks'] days_df = dataframes['days'] periods_df = dataframes['periods'] school_df_year_start = school_df[school_df['Identifier'] == 'AcademicYearStart']['Data'].iloc[0] school_df_year_end = school_df[school_df['Identifier'] == 'AcademicYearEnd']['Data'].iloc[0] if isinstance(school_df_year_start, str): school_year_start_date = datetime.strptime(school_df_year_start, '%Y-%m-%d') else: school_year_start_date = school_df_year_start if isinstance(school_df_year_end, str): school_year_end_date = datetime.strptime(school_df_year_end, '%Y-%m-%d') else: school_year_end_date = school_df_year_end # Create a dictionary to store the timetable nodes timetable_nodes = { 'timetable_node': None, 'academic_year_nodes': [], 'academic_term_nodes': [], 'academic_week_nodes': [], 'academic_day_nodes': [], 'academic_period_nodes': [] } # Create AcademicTimetable Node school_timetable_unique_id = f"{school_unique_id}_{school_year_start_date.year}_{school_year_end_date.year}" school_timetable_node = timetable.SchoolTimetableNode( school_timetable_id=school_timetable_unique_id, unique_id=school_timetable_unique_id, start_date=school_year_start_date, end_date=school_year_end_date, tldraw_snapshot="" ) neon.create_or_merge_neontology_node(school_timetable_node, database=db_name, operation='merge') timetable_nodes['timetable_node'] = school_timetable_node if school_node: logger.info(f"Creating calendar for {school_unique_id} from Neo4j SchoolNode: {school_node.unique_id}") calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=True, owner_node=school_node) # Link the school node to the timetable node neon.create_or_merge_neontology_relationship( entity_tt_rels.SchoolHasTimetable(source=school_node, target=school_timetable_node), database=db_name, operation='merge' ) timetable_nodes['calendar_nodes'] = calendar_nodes else: logger.info(f"Creating calendar for {school_unique_id} from dataframe SchoolID: {school_unique_id}") calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=False, owner_node=None) # Create AcademicYear nodes for each year within the range for year in range(school_year_start_date.year, school_year_end_date.year + 1): year_str = str(year) academic_year_unique_id = f"{school_timetable_unique_id}_{year}" academic_year_node = timetable.AcademicYearNode( unique_id=academic_year_unique_id, year=year_str, tldraw_snapshot="" ) neon.create_or_merge_neontology_node(academic_year_node, database=db_name, operation='merge') timetable_nodes['academic_year_nodes'].append(academic_year_node) logger.info(f'Created academic year node: {academic_year_node.unique_id}') neon.create_or_merge_neontology_relationship( tt_rels.AcademicTimetableHasAcademicYear(source=school_timetable_node, target=academic_year_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {school_timetable_node.unique_id} to {academic_year_node.unique_id}") # Link the academic year with the corresponding calendar year node for year_node in calendar_nodes['calendar_year_nodes']: if year_node.year == year: neon.create_or_merge_neontology_relationship( cal_tt_rels.AcademicYearIsCalendarYear(source=academic_year_node, target=year_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {year_node.unique_id}") break # Create Term and TermBreak nodes linked to AcademicYear term_number = 1 academic_term_number = 1 for _, term_row in terms_df.iterrows(): term_node_class = timetable.AcademicTermNode if term_row['TermType'] == 'Term' else timetable.AcademicTermBreakNode term_name = term_row['TermName'] term_name_no_spaces = term_name.replace(' ', '') term_start_date = term_row['StartDate'] if isinstance(term_start_date, pd.Timestamp): term_start_date = term_start_date.strftime('%Y-%m-%d') term_end_date = term_row['EndDate'] if isinstance(term_end_date, pd.Timestamp): term_end_date = term_end_date.strftime('%Y-%m-%d') if term_row['TermType'] == 'Term': term_node_unique_id = f"{school_timetable_unique_id}_{academic_term_number}_{term_name_no_spaces}" academic_term_number_str = str(academic_term_number) term_node = term_node_class( unique_id=term_node_unique_id, term_name=term_name, term_number=academic_term_number_str, start_date=datetime.strptime(term_start_date, '%Y-%m-%d'), end_date=datetime.strptime(term_end_date, '%Y-%m-%d'), tldraw_snapshot="" ) academic_term_number += 1 else: term_break_node_unique_id = f"{school_timetable_unique_id}_{term_name_no_spaces}" term_node = term_node_class( unique_id=term_break_node_unique_id, term_break_name=term_name, start_date=datetime.strptime(term_start_date, '%Y-%m-%d'), end_date=datetime.strptime(term_end_date, '%Y-%m-%d'), tldraw_snapshot="" ) neon.create_or_merge_neontology_node(term_node, database=db_name, operation='merge') logger.info(f'Created academic term break node: {term_node.unique_id}') timetable_nodes['academic_term_nodes'].append(term_node) term_number += 1 # We don't use this but we could # Link term node to the correct academic year term_years = set() term_years.update([term_node.start_date.year, term_node.end_date.year]) for academic_year_node in timetable_nodes['academic_year_nodes']: if int(academic_year_node.year) in term_years: relationship_class = tt_rels.AcademicYearHasAcademicTerm if term_row['TermType'] == 'Term' else tt_rels.AcademicYearHasAcademicTermBreak neon.create_or_merge_neontology_relationship( relationship_class(source=academic_year_node, target=term_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {term_node.unique_id}") # Create Week nodes academic_week_number = 1 for _, week_row in weeks_df.iterrows(): week_node_class = timetable.HolidayWeekNode if week_row['WeekType'] == 'Holiday' else timetable.AcademicWeekNode week_start_date = week_row['WeekStart'] if isinstance(week_start_date, pd.Timestamp): week_start_date = week_start_date.strftime('%Y-%m-%d') week_node_unique_id = f"{school_timetable_unique_id}_{week_row['WeekNumber']}_{week_row['WeekType']}Week" if week_row['WeekType'] == 'Holiday': week_node = week_node_class( unique_id=week_node_unique_id, start_date=datetime.strptime(week_start_date, '%Y-%m-%d'), tldraw_snapshot="" ) else: academic_week_number_str = str(academic_week_number) week_type = week_row['WeekType'] week_node = week_node_class( unique_id=week_node_unique_id, academic_week_number=academic_week_number_str, start_date=datetime.strptime(week_start_date, '%Y-%m-%d'), week_type=week_type, tldraw_snapshot="" ) academic_week_number += 1 neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge') timetable_nodes['academic_week_nodes'].append(week_node) logger.info(f"Created week node: {week_node.unique_id}") for calendar_node in calendar_nodes['calendar_week_nodes']: if calendar_node.start_date == week_node.start_date: if isinstance(week_node, timetable.AcademicWeekNode): neon.create_or_merge_neontology_relationship( cal_tt_rels.AcademicWeekIsCalendarWeek(source=week_node, target=calendar_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {calendar_node.unique_id} to {week_node.unique_id}") elif isinstance(week_node, timetable.HolidayWeekNode): neon.create_or_merge_neontology_relationship( cal_tt_rels.HolidayWeekIsCalendarWeek(source=week_node, target=calendar_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {calendar_node.unique_id} to {week_node.unique_id}") break # Link week node to the correct academic term for term_node in timetable_nodes['academic_term_nodes']: if term_node.start_date <= week_node.start_date <= term_node.end_date: relationship_class = tt_rels.AcademicTermHasAcademicWeek if week_row['WeekType'] != 'Holiday' else tt_rels.AcademicTermBreakHasHolidayWeek neon.create_or_merge_neontology_relationship( relationship_class(source=term_node, target=week_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {term_node.unique_id} to {week_node.unique_id}") break # Link week node to the correct academic year for academic_year_node in timetable_nodes['academic_year_nodes']: if int(academic_year_node.year) == week_node.start_date.year: relationship_class = tt_rels.AcademicYearHasAcademicWeek if week_row['WeekType'] != 'Holiday' else tt_rels.AcademicYearHasHolidayWeek neon.create_or_merge_neontology_relationship( relationship_class(source=academic_year_node, target=week_node), database=db_name, operation='merge' ) logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {week_node.unique_id}") break # Create Day nodes day_number = 1 academic_day_number = 1 for _, day_row in days_df.iterrows(): date_str = day_row['Date'] if isinstance(date_str, pd.Timestamp): date_str = date_str.strftime('%Y-%m-%d') day_node_class = { 'Academic': timetable.AcademicDayNode, 'Holiday': timetable.HolidayDayNode, 'OffTimetable': timetable.OffTimetableDayNode, 'StaffDay': timetable.StaffDayNode }[day_row['DayType']] # Format the unique ID as {day_node_class.__name__}Day day_node_data = { 'unique_id': f"{school_timetable_unique_id}_{day_number}_{day_node_class.__name__}Day", 'date': datetime.strptime(date_str, '%Y-%m-%d'), 'day_of_week': datetime.strptime(date_str, '%Y-%m-%d').strftime('%A'), 'tldraw_snapshot': "" } if day_row['DayType'] == 'Academic': day_node_data['academic_day'] = str(academic_day_number) day_node_data['day_type'] = day_row['WeekType'] day_node_data['tldraw_snapshot'] = "" day_node = day_node_class(**day_node_data) for calendar_node in calendar_nodes['calendar_day_nodes']: if calendar_node.date == day_node.date: neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge') timetable_nodes['academic_day_nodes'].append(day_node) logger.info(f"Created day node: {day_node.unique_id}") if isinstance(day_node, timetable.AcademicDayNode): relationship_class = cal_tt_rels.AcademicDayIsCalendarDay elif isinstance(day_node, timetable.HolidayDayNode): relationship_class = cal_tt_rels.HolidayDayIsCalendarDay elif isinstance(day_node, timetable.OffTimetableDayNode): relationship_class = cal_tt_rels.OffTimetableDayIsCalendarDay elif isinstance(day_node, timetable.StaffDayNode): relationship_class = cal_tt_rels.StaffDayIsCalendarDay neon.create_or_merge_neontology_relationship( relationship_class(source=day_node, target=calendar_node), database=db_name, operation='merge' ) logger.info(f'Created relationship from {calendar_node.unique_id} to {day_node.unique_id}') break # Link day node to the correct academic week for academic_week_node in timetable_nodes['academic_week_nodes']: if academic_week_node.start_date <= day_node.date <= (academic_week_node.start_date + timedelta(days=6)): if day_row['DayType'] == 'Academic': relationship_class = tt_rels.AcademicWeekHasAcademicDay elif day_row['DayType'] == 'Holiday': if hasattr(academic_week_node, 'week_type') and academic_week_node.week_type in ['A', 'B']: relationship_class = tt_rels.AcademicWeekHasHolidayDay else: relationship_class = tt_rels.HolidayWeekHasHolidayDay elif day_row['DayType'] == 'OffTimetable': relationship_class = tt_rels.AcademicWeekHasOffTimetableDay elif day_row['DayType'] == 'Staff': relationship_class = tt_rels.AcademicWeekHasStaffDay else: continue # Skip linking for other day types neon.create_or_merge_neontology_relationship( relationship_class(source=academic_week_node, target=day_node), database=db_name, operation='merge' ) logger.info(f"Created relationship from {academic_week_node.unique_id} to {day_node.unique_id}") break # Link day node to the correct academic term for term_node in timetable_nodes['academic_term_nodes']: if term_node.start_date <= day_node.date <= term_node.end_date: if day_row['DayType'] == 'Academic': relationship_class = tt_rels.AcademicTermHasAcademicDay elif day_row['DayType'] == 'Holiday': if isinstance(term_node, timetable.AcademicTermNode): relationship_class = tt_rels.AcademicTermHasHolidayDay else: relationship_class = tt_rels.AcademicTermBreakHasHolidayDay elif day_row['DayType'] == 'OffTimetable': relationship_class = tt_rels.AcademicTermHasOffTimetableDay elif day_row['DayType'] == 'Staff': relationship_class = tt_rels.AcademicTermHasStaffDay else: continue # Skip linking for other day types neon.create_or_merge_neontology_relationship( relationship_class(source=term_node, target=day_node), database=db_name, operation='merge' ) logger.info(f"Created relationship from {term_node.unique_id} to {day_node.unique_id}") break # Create Period nodes for each academic day if day_row['DayType'] == 'Academic': logger.info(f"Creating periods for {day_node.unique_id}") period_of_day = 1 academic_or_registration_period_of_day = 1 for _, period_row in periods_df.iterrows(): period_node_class = { 'Academic': timetable.AcademicPeriodNode, 'Registration': timetable.RegistrationPeriodNode, 'Break': timetable.BreakPeriodNode, 'OffTimetable': timetable.OffTimetablePeriodNode }[period_row['PeriodType']] logger.info(f"Creating period node for {period_node_class.__name__} Period: {period_of_day}") period_node_unique_id = f"{school_timetable_unique_id}_{academic_day_number}_{period_of_day}_{period_node_class.__name__}Period" logger.debug(f"Period node unique id: {period_node_unique_id}") period_node_data = { 'unique_id': period_node_unique_id, 'name': period_row['PeriodName'], 'date': day_node.date, 'start_time': datetime.combine(day_node.date, period_row['StartTime']), 'end_time': datetime.combine(day_node.date, period_row['EndTime']), 'tldraw_snapshot': "" } logger.debug(f"Period node data: {period_node_data}") if period_row['PeriodType'] in ['Academic', 'Registration']: week_type = day_row['WeekType'] day_name_short = day_node.day_of_week[:3] period_code = period_row['PeriodCode'] period_code_formatted = f"{week_type}{day_name_short}{period_code}" period_node_data['period_code'] = period_code_formatted period_node_data['tldraw_snapshot'] = "" academic_or_registration_period_of_day += 1 period_node = period_node_class(**period_node_data) neon.create_or_merge_neontology_node(period_node, database=db_name, operation='merge') timetable_nodes['academic_period_nodes'].append(period_node) logger.info(f'Created period node: {period_node.unique_id}') relationship_class = { 'Academic': tt_rels.AcademicDayHasAcademicPeriod, 'Registration': tt_rels.AcademicDayHasRegistrationPeriod, 'Break': tt_rels.AcademicDayHasBreakPeriod, 'OffTimetable': tt_rels.AcademicDayHasOffTimetablePeriod }[period_row['PeriodType']] neon.create_or_merge_neontology_relationship( relationship_class(source=day_node, target=period_node), database=db_name, operation='merge' ) logger.info(f"Created relationship from {day_node.unique_id} to {period_node.unique_id}") period_of_day += 1 # We don't use this but we could academic_day_number += 1 # This is a bit of a hack but it works to keep the directories aligned (reorganise) day_number += 1 # We don't use this but we could def create_school_timetable_node_sequence_rels(timetable_nodes): def sort_and_create_relationships(nodes, relationship_map, sort_key): sorted_nodes = sorted(nodes, key=sort_key) for i in range(len(sorted_nodes) - 1): source_node = sorted_nodes[i] target_node = sorted_nodes[i + 1] node_type_pair = (type(source_node), type(target_node)) relationship_class = relationship_map.get(node_type_pair) if relationship_class: # Avoid self-referential relationships if source_node.unique_id != target_node.unique_id: neon.create_or_merge_neontology_relationship( relationship_class( source=source_node, target=target_node ), database=db_name, operation='merge' ) logger.info(f"Created relationship from {source_node.unique_id} to {target_node.unique_id}") else: logger.warning(f"Skipped self-referential relationship for node {source_node.unique_id}") # Relationship maps for different node types academic_year_relationship_map = { (timetable.AcademicYearNode, timetable.AcademicYearNode): tt_rels.AcademicYearFollowsAcademicYear } academic_term_relationship_map = { (timetable.AcademicTermNode, timetable.AcademicTermBreakNode): tt_rels.AcademicTermBreakFollowsAcademicTerm, (timetable.AcademicTermBreakNode, timetable.AcademicTermNode): tt_rels.AcademicTermFollowsAcademicTermBreak } academic_week_relationship_map = { (timetable.AcademicWeekNode, timetable.AcademicWeekNode): tt_rels.AcademicWeekFollowsAcademicWeek, (timetable.HolidayWeekNode, timetable.HolidayWeekNode): tt_rels.HolidayWeekFollowsHolidayWeek, (timetable.AcademicWeekNode, timetable.HolidayWeekNode): tt_rels.HolidayWeekFollowsAcademicWeek, (timetable.HolidayWeekNode, timetable.AcademicWeekNode): tt_rels.AcademicWeekFollowsHolidayWeek } academic_day_relationship_map = { (timetable.AcademicDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsAcademicDay, (timetable.HolidayDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsHolidayDay, (timetable.OffTimetableDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsOffTimetableDay, (timetable.StaffDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsStaffDay, (timetable.AcademicDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsAcademicDay, (timetable.AcademicDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsAcademicDay, (timetable.AcademicDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsAcademicDay, (timetable.HolidayDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsHolidayDay, (timetable.HolidayDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsHolidayDay, (timetable.HolidayDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsHolidayDay, (timetable.OffTimetableDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsOffTimetableDay, (timetable.OffTimetableDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsOffTimetableDay, (timetable.OffTimetableDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsOffTimetableDay, (timetable.StaffDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsStaffDay, (timetable.StaffDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsStaffDay, (timetable.StaffDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsStaffDay, } academic_period_relationship_map = { (timetable.AcademicPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsAcademicPeriod, (timetable.AcademicPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsAcademicPeriod, (timetable.AcademicPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsAcademicPeriod, (timetable.AcademicPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsAcademicPeriod, (timetable.BreakPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsBreakPeriod, (timetable.BreakPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsBreakPeriod, (timetable.BreakPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsBreakPeriod, (timetable.BreakPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsBreakPeriod, (timetable.RegistrationPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsRegistrationPeriod, (timetable.RegistrationPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsRegistrationPeriod, (timetable.RegistrationPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsRegistrationPeriod, (timetable.RegistrationPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsRegistrationPeriod, (timetable.OffTimetablePeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsOffTimetablePeriod, (timetable.OffTimetablePeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsOffTimetablePeriod, (timetable.OffTimetablePeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsOffTimetablePeriod, (timetable.OffTimetablePeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsOffTimetablePeriod, } # Sort and create relationships sort_and_create_relationships(timetable_nodes['academic_year_nodes'], academic_year_relationship_map, lambda x: int(x.year)) sort_and_create_relationships(timetable_nodes['academic_term_nodes'], academic_term_relationship_map, lambda x: x.start_date) sort_and_create_relationships(timetable_nodes['academic_week_nodes'], academic_week_relationship_map, lambda x: x.start_date) sort_and_create_relationships(timetable_nodes['academic_day_nodes'], academic_day_relationship_map, lambda x: x.date) sort_and_create_relationships(timetable_nodes['academic_period_nodes'], academic_period_relationship_map, lambda x: (x.start_time, x.end_time)) # Call the function with the created timetable nodes create_school_timetable_node_sequence_rels(timetable_nodes) logger.info(f'Created timetable: {timetable_nodes["timetable_node"].unique_id}') # Log the directory structure after creation # root_timetable_directory = fs_handler.root_path # Access the root directory of the filesystem handler # fs_handler.log_directory_structure(root_timetable_directory) return { 'school_node': school_node, 'school_calendar_nodes': calendar_nodes, 'school_timetable_nodes': timetable_nodes }