This commit is contained in:
2025-11-14 14:47:19 +00:00
parent 2a85845835
commit 3758c7572a
137 changed files with 365654 additions and 11147 deletions
+101 -54
View File
@@ -11,7 +11,7 @@ import modules.database.schemas.relationships.calendar_timetable_rels as cal_tt_
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):
def create_school_timetable(dataframes, db_name, school_node=None, filesystem=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.")
@@ -22,10 +22,10 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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]
school_uuid_string = 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
school_uuid_string = school_node.uuid_string
terms_df = dataframes['terms']
weeks_df = dataframes['weeks']
@@ -54,20 +54,30 @@ def create_school_timetable(dataframes, db_name, school_node=None):
}
# Create AcademicTimetable Node
school_timetable_unique_id = f"{school_unique_id}_{school_year_start_date.year}_{school_year_end_date.year}"
school_timetable_uuid_string = f"{school_uuid_string}_{school_year_start_date.year}_{school_year_end_date.year}"
# Generate storage path for timetable node
if filesystem:
timetable_dir_created, timetable_path = filesystem.create_school_timetable_directory()
node_storage_path = os.path.relpath(timetable_path, filesystem.base_path)
logger.info(f"Generated timetable node_storage_path: {node_storage_path}")
else:
node_storage_path = ""
logger.warning("No filesystem provided, using empty storage path")
school_timetable_node = timetable.SchoolTimetableNode(
school_timetable_id=school_timetable_unique_id,
unique_id=school_timetable_unique_id,
school_timetable_id=school_timetable_uuid_string,
uuid_string=school_timetable_uuid_string,
start_date=school_year_start_date,
end_date=school_year_end_date,
tldraw_snapshot=""
node_storage_path=node_storage_path
)
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)
logger.info(f"Creating calendar for {school_uuid_string} from Neo4j SchoolNode: {school_node.uuid_string}")
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, filesystem=filesystem)
# 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),
@@ -75,26 +85,34 @@ def create_school_timetable(dataframes, db_name, school_node=None):
)
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)
logger.info(f"Creating calendar for {school_uuid_string} from dataframe SchoolID: {school_uuid_string}")
calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=False, owner_node=None, filesystem=filesystem)
# 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_uuid_string = f"{school_timetable_uuid_string}_{year}"
# Generate storage path for academic year node
if filesystem:
year_dir_created, year_path = filesystem.create_school_timetable_year_directory(timetable_path, year)
node_storage_path = os.path.relpath(year_path, filesystem.base_path)
else:
node_storage_path = ""
academic_year_node = timetable.AcademicYearNode(
unique_id=academic_year_unique_id,
uuid_string=academic_year_uuid_string,
year=year_str,
tldraw_snapshot=""
node_storage_path=node_storage_path
)
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}')
logger.info(f'Created academic year node: {academic_year_node.uuid_string}')
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}")
logger.info(f"Created school timetable relationship from {school_timetable_node.uuid_string} to {academic_year_node.uuid_string}")
# Link the academic year with the corresponding calendar year node
for year_node in calendar_nodes['calendar_year_nodes']:
@@ -103,7 +121,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created school timetable relationship from {academic_year_node.uuid_string} to {year_node.uuid_string}")
break
# Create Term and TermBreak nodes linked to AcademicYear
@@ -121,29 +139,39 @@ def create_school_timetable(dataframes, db_name, school_node=None):
if isinstance(term_end_date, pd.Timestamp):
term_end_date = term_end_date.strftime('%Y-%m-%d')
# Generate storage path for term node
if filesystem:
if term_row['TermType'] == 'Term':
term_dir_created, term_path = filesystem.create_school_timetable_academic_term_directory(timetable_path, term_name, academic_term_number)
else:
term_dir_created, term_path = filesystem.create_school_timetable_academic_term_break_directory(timetable_path, term_name)
node_storage_path = os.path.relpath(term_path, filesystem.base_path)
else:
node_storage_path = ""
if term_row['TermType'] == 'Term':
term_node_unique_id = f"{school_timetable_unique_id}_{academic_term_number}_{term_name_no_spaces}"
term_node_uuid_string = f"{school_timetable_uuid_string}_{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,
uuid_string=term_node_uuid_string,
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=""
node_storage_path=node_storage_path
)
academic_term_number += 1
else:
term_break_node_unique_id = f"{school_timetable_unique_id}_{term_name_no_spaces}"
term_break_node_uuid_string = f"{school_timetable_uuid_string}_{term_name_no_spaces}"
term_node = term_node_class(
unique_id=term_break_node_unique_id,
uuid_string=term_break_node_uuid_string,
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=""
node_storage_path=node_storage_path
)
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}')
logger.info(f'Created academic term break node: {term_node.uuid_string}')
timetable_nodes['academic_term_nodes'].append(term_node)
term_number += 1 # We don't use this but we could
@@ -158,7 +186,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created school timetable relationship from {academic_year_node.uuid_string} to {term_node.uuid_string}")
# Create Week nodes
academic_week_number = 1
@@ -168,28 +196,35 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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"
week_node_uuid_string = f"{school_timetable_uuid_string}_{week_row['WeekNumber']}_{week_row['WeekType']}Week"
# Generate storage path for week node
if filesystem:
week_dir_created, week_path = filesystem.create_school_timetable_academic_week_directory(timetable_path, week_row['WeekNumber'])
node_storage_path = os.path.relpath(week_path, filesystem.base_path)
else:
node_storage_path = ""
if week_row['WeekType'] == 'Holiday':
week_node = week_node_class(
unique_id=week_node_unique_id,
uuid_string=week_node_uuid_string,
start_date=datetime.strptime(week_start_date, '%Y-%m-%d'),
tldraw_snapshot=""
node_storage_path=node_storage_path
)
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,
uuid_string=week_node_uuid_string,
academic_week_number=academic_week_number_str,
start_date=datetime.strptime(week_start_date, '%Y-%m-%d'),
week_type=week_type,
tldraw_snapshot=""
node_storage_path=node_storage_path
)
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}")
logger.info(f"Created week node: {week_node.uuid_string}")
for calendar_node in calendar_nodes['calendar_week_nodes']:
if calendar_node.start_date == week_node.start_date:
if isinstance(week_node, timetable.AcademicWeekNode):
@@ -197,13 +232,13 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created school timetable relationship from {calendar_node.uuid_string} to {week_node.uuid_string}")
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}")
logger.info(f"Created school timetable relationship from {calendar_node.uuid_string} to {week_node.uuid_string}")
break
# Link week node to the correct academic term
@@ -214,7 +249,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created school timetable relationship from {term_node.uuid_string} to {week_node.uuid_string}")
break
# Link week node to the correct academic year
@@ -225,7 +260,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created school timetable relationship from {academic_year_node.uuid_string} to {week_node.uuid_string}")
break
# Create Day nodes
@@ -243,18 +278,24 @@ def create_school_timetable(dataframes, db_name, school_node=None):
'StaffDay': timetable.StaffDayNode
}[day_row['DayType']]
# Generate storage path for day node
if filesystem:
day_dir_created, day_path = filesystem.create_school_timetable_academic_day_directory(timetable_path, academic_day_number)
node_storage_path = os.path.relpath(day_path, filesystem.base_path)
else:
node_storage_path = ""
# 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",
'uuid_string': f"{school_timetable_uuid_string}_{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': ""
'node_storage_path': node_storage_path
}
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)
@@ -262,7 +303,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created day node: {day_node.uuid_string}")
if isinstance(day_node, timetable.AcademicDayNode):
relationship_class = cal_tt_rels.AcademicDayIsCalendarDay
@@ -277,7 +318,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}')
logger.info(f'Created relationship from {calendar_node.uuid_string} to {day_node.uuid_string}')
break
# Link day node to the correct academic week
@@ -300,7 +341,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created relationship from {academic_week_node.uuid_string} to {day_node.uuid_string}")
break
# Link day node to the correct academic term
@@ -323,12 +364,12 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created relationship from {term_node.uuid_string} to {day_node.uuid_string}")
break
# Create Period nodes for each academic day
if day_row['DayType'] == 'Academic':
logger.info(f"Creating periods for {day_node.unique_id}")
logger.info(f"Creating periods for {day_node.uuid_string}")
period_of_day = 1
academic_or_registration_period_of_day = 1
for _, period_row in periods_df.iterrows():
@@ -340,15 +381,22 @@ def create_school_timetable(dataframes, db_name, school_node=None):
}[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_uuid_string = f"{school_timetable_uuid_string}_{academic_day_number}_{period_of_day}_{period_node_class.__name__}Period"
logger.debug(f"Period node unique id: {period_node_uuid_string}")
# Generate storage path for period node
if filesystem:
period_dir_created, period_path = filesystem.create_school_timetable_period_directory(timetable_path, academic_day_number, period_row['PeriodCode'])
node_storage_path = os.path.relpath(period_path, filesystem.base_path)
else:
node_storage_path = ""
period_node_data = {
'unique_id': period_node_unique_id,
'uuid_string': period_node_uuid_string,
'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': ""
'node_storage_path': node_storage_path
}
logger.debug(f"Period node data: {period_node_data}")
if period_row['PeriodType'] in ['Academic', 'Registration']:
@@ -357,14 +405,13 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}')
logger.info(f'Created period node: {period_node.uuid_string}')
relationship_class = {
'Academic': tt_rels.AcademicDayHasAcademicPeriod,
@@ -377,7 +424,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
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}")
logger.info(f"Created relationship from {day_node.uuid_string} to {period_node.uuid_string}")
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
@@ -392,7 +439,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
relationship_class = relationship_map.get(node_type_pair)
if relationship_class:
# Avoid self-referential relationships
if source_node.unique_id != target_node.unique_id:
if source_node.uuid_string != target_node.uuid_string:
neon.create_or_merge_neontology_relationship(
relationship_class(
source=source_node,
@@ -400,9 +447,9 @@ def create_school_timetable(dataframes, db_name, school_node=None):
),
database=db_name, operation='merge'
)
logger.info(f"Created relationship from {source_node.unique_id} to {target_node.unique_id}")
logger.info(f"Created relationship from {source_node.uuid_string} to {target_node.uuid_string}")
else:
logger.warning(f"Skipped self-referential relationship for node {source_node.unique_id}")
logger.warning(f"Skipped self-referential relationship for node {source_node.uuid_string}")
# Relationship maps for different node types
academic_year_relationship_map = {
@@ -474,7 +521,7 @@ def create_school_timetable(dataframes, db_name, school_node=None):
# 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}')
logger.info(f'Created timetable: {timetable_nodes["timetable_node"].uuid_string}')
# Log the directory structure after creation
# root_timetable_directory = fs_handler.root_path # Access the root directory of the filesystem handler