Initial commit

This commit is contained in:
2025-07-11 13:52:19 +00:00
commit e0c489f625
362 changed files with 27286 additions and 0 deletions
View File
View File
+280
View File
@@ -0,0 +1,280 @@
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.schemas.nodes.calendars as calendar_schemas
import modules.database.schemas.entities as entities
import modules.database.schemas.relationships.calendars as cal_rels
import modules.database.tools.neontology_tools as neon
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
from datetime import timedelta, datetime
def create_calendar(db_name, start_date, end_date, attach_to_calendar_node=False, entity_node=None, time_chunk_node=None):
logger.info(f"Creating calendar for {start_date} to {end_date}")
logger.info("Initializing Neontology connection")
neon.init_neontology_connection()
filesystem = ClassroomCopilotFilesystem(db_name, init_run_type="school")
def create_tldraw_file_for_node(node, node_path):
node_data = {
"unique_id": node.unique_id,
"type": node.__class__.__name__,
"name": node.name if hasattr(node, 'name') else 'Unnamed Node'
}
logger.debug(f"Creating tldraw file for node: {node_data}")
filesystem.create_default_tldraw_file(node_path, node_data)
created_years = {}
created_months = {}
created_weeks = {}
created_days = {}
last_year_node = None
last_month_node = None
last_week_node = None
last_day_node = None
calendar_nodes = {
'calendar_node': None,
'calendar_year_nodes': [],
'calendar_month_nodes': [],
'calendar_week_nodes': [],
'calendar_day_nodes': []
}
calendar_type = None
if attach_to_calendar_node and entity_node:
calendar_type = "entity_calendar"
logger.info(f"Attaching calendar to entity node: {entity_node.unique_id}")
entity_unique_id = entity_node.unique_id
calendar_unique_id = f"Calendar_{entity_unique_id}"
calendar_name = f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
calendar_path = os.path.join(entity_node.path, "calendar")
calendar_node = calendar_schemas.CalendarNode(
unique_id=calendar_unique_id,
name=calendar_name,
start_date=start_date,
end_date=end_date,
path=calendar_path
)
neon.create_or_merge_neontology_node(calendar_node, database=db_name, operation='merge')
calendar_nodes['calendar_node'] = calendar_node
logger.info(f"Calendar node created: {calendar_node.unique_id}")
# Create a node tldraw file for the calendar node
create_tldraw_file_for_node(calendar_node, calendar_path)
import backend.modules.database.schemas.relationships.owner_relationships as entity_cal_rels
neon.create_or_merge_neontology_relationship(
entity_cal_rels.EntityHasCalendar(source=entity_node, target=calendar_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {entity_node.unique_id} to {calendar_node.unique_id}")
if entity_node and not attach_to_calendar_node:
calendar_type = "time_entity"
else:
logger.error("Invalid combination of parameters for calendar creation.")
raise ValueError("Invalid combination of parameters for calendar creation.")
current_date = start_date
while current_date <= end_date:
year = current_date.year
month = current_date.month
day = current_date.day
iso_year, iso_week, iso_weekday = current_date.isocalendar()
# Create directories for year, month, week, and day
_, year_path = filesystem.create_year_directory(year, calendar_path)
_, month_path = filesystem.create_month_directory(year, month, calendar_path)
_, week_path = filesystem.create_week_directory(year, iso_week, calendar_path)
_, day_path = filesystem.create_day_directory(year, month, day, calendar_path)
calendar_year_unique_id = f"CalendarYear_{year}"
if year not in created_years:
year_node = calendar_schemas.CalendarYearNode(
unique_id=calendar_year_unique_id,
year=str(year),
path=year_path
)
neon.create_or_merge_neontology_node(year_node, database=db_name, operation='merge')
calendar_nodes['calendar_year_nodes'].append(year_node)
created_years[year] = year_node
create_tldraw_file_for_node(year_node, year_path)
logger.info(f"Year node created: {year_node.unique_id}")
if attach_to_calendar_node:
neon.create_or_merge_neontology_relationship(
cal_rels.CalendarIncludesYear(source=calendar_node, target=year_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {calendar_node.unique_id} to {year_node.unique_id}")
if last_year_node:
neon.create_or_merge_neontology_relationship(
cal_rels.YearFollowsYear(source=last_year_node, target=year_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_year_node.unique_id} to {year_node.unique_id}")
last_year_node = year_node
calendar_month_unique_id = f"CalendarMonth_{year}_{month}"
month_key = f"{year}-{month}"
if month_key not in created_months:
month_node = calendar_schemas.CalendarMonthNode(
unique_id=calendar_month_unique_id,
year=str(year),
month=str(month),
month_name=datetime(year, month, 1).strftime('%B'),
path=month_path
)
neon.create_or_merge_neontology_node(month_node, database=db_name, operation='merge')
calendar_nodes['calendar_month_nodes'].append(month_node)
created_months[month_key] = month_node
create_tldraw_file_for_node(month_node, month_path)
logger.info(f"Month node created: {month_node.unique_id}")
# Check for the end of year transition for months
if last_month_node:
if int(month) == 1 and int(last_month_node.month) == 12 and int(last_month_node.year) == year - 1:
neon.create_or_merge_neontology_relationship(
cal_rels.MonthFollowsMonth(source=last_month_node, target=month_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}")
elif int(month) == int(last_month_node.month) + 1:
neon.create_or_merge_neontology_relationship(
cal_rels.MonthFollowsMonth(source=last_month_node, target=month_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}")
last_month_node = month_node
neon.create_or_merge_neontology_relationship(
cal_rels.YearIncludesMonth(source=year_node, target=month_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {year_node.unique_id} to {month_node.unique_id}")
calendar_week_unique_id = f"CalendarWeek_{iso_year}_{iso_week}"
week_key = f"{iso_year}-W{iso_week}"
if week_key not in created_weeks:
# Get the date of the first monday of the week
week_start_date = current_date - timedelta(days=current_date.weekday())
week_node = calendar_schemas.CalendarWeekNode(
unique_id=calendar_week_unique_id,
start_date=week_start_date,
week_number=str(iso_week),
iso_week=f"{iso_year}-W{iso_week:02}",
path=week_path
)
neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge')
calendar_nodes['calendar_week_nodes'].append(week_node)
created_weeks[week_key] = week_node
create_tldraw_file_for_node(week_node, week_path)
logger.info(f"Week node created: {week_node.unique_id}")
if last_week_node and ((last_week_node.iso_week.split('-')[0] == str(iso_year) and int(last_week_node.week_number) == int(iso_week) - 1) or
(last_week_node.iso_week.split('-')[0] != str(iso_year) and int(last_week_node.week_number) == 52 and int(iso_week) == 1)):
neon.create_or_merge_neontology_relationship(
cal_rels.WeekFollowsWeek(source=last_week_node, target=week_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_week_node.unique_id} to {week_node.unique_id}")
last_week_node = week_node
neon.create_or_merge_neontology_relationship(
cal_rels.YearIncludesWeek(source=year_node, target=week_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {year_node.unique_id} to {week_node.unique_id}")
calendar_day_unique_id = f"CalendarDay_{year}_{month}_{day}"
day_key = f"{year}-{month}-{day}"
day_node = calendar_schemas.CalendarDayNode(
unique_id=calendar_day_unique_id,
date=current_date,
day_of_week=current_date.strftime('%A'),
iso_day=f"{year}-{month:02}-{day:02}",
path=day_path
)
neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge')
calendar_nodes['calendar_day_nodes'].append(day_node)
created_days[day_key] = day_node
create_tldraw_file_for_node(day_node, day_path)
logger.info(f"Day node created: {day_node.unique_id}")
if last_day_node:
neon.create_or_merge_neontology_relationship(
cal_rels.DayFollowsDay(source=last_day_node, target=day_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_day_node.unique_id} to {day_node.unique_id}")
last_day_node = day_node
neon.create_or_merge_neontology_relationship(
cal_rels.MonthIncludesDay(source=month_node, target=day_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {month_node.unique_id} to {day_node.unique_id}")
neon.create_or_merge_neontology_relationship(
cal_rels.WeekIncludesDay(source=week_node, target=day_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {week_node.unique_id} to {day_node.unique_id}")
current_date += timedelta(days=1)
if time_chunk_node:
time_chunk_interval = time_chunk_node
# Get every calendar day node and create time chunks of length time_chunk_node minutes for the whole day
for day_node in calendar_nodes['calendar_day_nodes']:
day_path = day_node.path
total_time_chunks_in_day = (24 * 60) / time_chunk_interval
for i in range(total_time_chunks_in_day):
time_chunk_unique_id = f"CalendarTimeChunk_{day_node.unique_id}_{i}"
time_chunk_start_time = day_node.date.time() + timedelta(minutes=i * time_chunk_interval)
time_chunk_end_time = time_chunk_start_time + timedelta(minutes=time_chunk_interval)
time_chunk_node = calendar_schemas.CalendarTimeChunkNode(
unique_id=time_chunk_unique_id,
start_time=time_chunk_start_time,
end_time=time_chunk_end_time,
path=day_path
)
neon.create_or_merge_neontology_node(time_chunk_node, database=db_name, operation='merge')
calendar_nodes['calendar_time_chunk_nodes'].append(time_chunk_node)
logger.info(f"Time chunk node created: {time_chunk_node.unique_id}")
# Create a relationship between the time chunk node and the day node
neon.create_or_merge_neontology_relationship(
cal_rels.DayIncludesTimeChunk(source=day_node, target=time_chunk_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {day_node.unique_id} to {time_chunk_node.unique_id}")
# Create sequential relationship between the time chunk nodes
if i > 0:
neon.create_or_merge_neontology_relationship(
cal_rels.TimeChunkFollowsTimeChunk(source=calendar_nodes['calendar_time_chunk_nodes'][i-1], target=time_chunk_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {calendar_nodes['calendar_time_chunk_nodes'][i-1].unique_id} to {time_chunk_node.unique_id}")
logger.info(f'Created calendar: {calendar_nodes["calendar_node"].unique_id}')
return calendar_nodes
+401
View File
@@ -0,0 +1,401 @@
from enum import Enum
from typing import Optional, List, Dict, Any
import logging
from modules.database.admin.neontology_provider import NeontologyProvider
class NodeLabels(Enum):
SCHOOL = "School"
DEPARTMENT_STRUCTURE = "DepartmentStructure"
CURRICULUM_STRUCTURE = "CurriculumStructure"
PASTORAL_STRUCTURE = "PastoralStructure"
DEPARTMENT = "Department"
KEY_STAGE = "KeyStage"
YEAR_GROUP = "YearGroup"
class RelationshipTypes(Enum):
HAS_DEPARTMENT_STRUCTURE = "HAS_DEPARTMENT_STRUCTURE"
HAS_CURRICULUM_STRUCTURE = "HAS_CURRICULUM_STRUCTURE"
HAS_PASTORAL_STRUCTURE = "HAS_PASTORAL_STRUCTURE"
HAS_DEPARTMENT = "HAS_DEPARTMENT"
INCLUDES_KEY_STAGE = "INCLUDES_KEY_STAGE"
INCLUDES_YEAR_GROUP = "INCLUDES_YEAR_GROUP"
class PropertyKeys(Enum):
UNIQUE_ID = "unique_id"
PATH = "path"
URN = "urn"
ESTABLISHMENT_NUMBER = "establishment_number"
ESTABLISHMENT_NAME = "establishment_name"
ESTABLISHMENT_TYPE = "establishment_type"
ESTABLISHMENT_STATUS = "establishment_status"
PHASE_OF_EDUCATION = "phase_of_education"
STATUTORY_LOW_AGE = "statutory_low_age"
STATUTORY_HIGH_AGE = "statutory_high_age"
RELIGIOUS_CHARACTER = "religious_character"
SCHOOL_CAPACITY = "school_capacity"
SCHOOL_WEBSITE = "school_website"
OFSTED_RATING = "ofsted_rating"
DEPARTMENT_NAME = "department_name"
KEY_STAGE = "key_stage"
KEY_STAGE_NAME = "key_stage_name"
YEAR_GROUP = "year_group"
YEAR_GROUP_NAME = "year_group_name"
CREATED = "created"
MERGED = "merged"
class SchemaDefinition:
"""Class to hold schema definition queries and information"""
@staticmethod
def get_schema_info() -> Dict[str, List[Dict]]:
"""Returns a dictionary containing the schema definition for nodes and relationships."""
return {
"nodes": [
{
"label": "School",
"description": "Represents a school entity",
"required_properties": ["unique_id", "urn", "name"],
"optional_properties": ["address", "postcode", "phone", "email", "website"]
},
{
"label": "DepartmentStructure",
"description": "Represents the department structure of a school",
"required_properties": ["unique_id", "name"],
"optional_properties": ["description", "head_of_department"]
},
{
"label": "CurriculumStructure",
"description": "Represents the curriculum structure of a school",
"required_properties": ["unique_id", "name"],
"optional_properties": ["description", "key_stage", "subject"]
},
{
"label": "PastoralStructure",
"description": "Represents the pastoral structure of a school",
"required_properties": ["unique_id", "name"],
"optional_properties": ["description", "year_group", "form_group"]
}
],
"relationships": [
{
"type": "HAS_DEPARTMENT_STRUCTURE",
"description": "Links a school to its department structure",
"source": "School",
"target": "DepartmentStructure",
"properties": ["created_at"]
},
{
"type": "HAS_CURRICULUM_STRUCTURE",
"description": "Links a school to its curriculum structure",
"source": "School",
"target": "CurriculumStructure",
"properties": ["created_at"]
},
{
"type": "HAS_PASTORAL_STRUCTURE",
"description": "Links a school to its pastoral structure",
"source": "School",
"target": "PastoralStructure",
"properties": ["created_at"]
}
]
}
@staticmethod
def get_schema_creation_queries() -> List[str]:
"""Returns a list of Cypher queries to create the schema."""
return [
# Node Uniqueness Constraints
f"CREATE CONSTRAINT school_unique_id IF NOT EXISTS FOR (n:{NodeLabels.SCHOOL.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE",
f"CREATE CONSTRAINT department_unique_id IF NOT EXISTS FOR (n:{NodeLabels.DEPARTMENT_STRUCTURE.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE",
f"CREATE CONSTRAINT curriculum_unique_id IF NOT EXISTS FOR (n:{NodeLabels.CURRICULUM_STRUCTURE.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE",
f"CREATE CONSTRAINT pastoral_unique_id IF NOT EXISTS FOR (n:{NodeLabels.PASTORAL_STRUCTURE.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE",
# Indexes for Performance
f"CREATE INDEX school_urn IF NOT EXISTS FOR (n:{NodeLabels.SCHOOL.value}) ON (n.{PropertyKeys.URN.value})",
f"CREATE INDEX school_name IF NOT EXISTS FOR (n:{NodeLabels.SCHOOL.value}) ON (n.{PropertyKeys.ESTABLISHMENT_NAME.value})",
f"CREATE INDEX department_name IF NOT EXISTS FOR (n:{NodeLabels.DEPARTMENT_STRUCTURE.value}) ON (n.{PropertyKeys.DEPARTMENT_NAME.value})",
f"CREATE INDEX curriculum_name IF NOT EXISTS FOR (n:{NodeLabels.CURRICULUM_STRUCTURE.value}) ON (n.name)",
f"CREATE INDEX pastoral_name IF NOT EXISTS FOR (n:{NodeLabels.PASTORAL_STRUCTURE.value}) ON (n.name)",
]
@staticmethod
def get_schema_verification_queries() -> Dict[str, str]:
"""Returns a dictionary of queries to verify the schema state."""
return {
"constraints": "SHOW CONSTRAINTS",
"indexes": "SHOW INDEXES",
"labels": "CALL db.labels()"
}
class GraphNamingProvider:
@staticmethod
def get_school_unique_id(urn: str) -> str:
"""Generate unique ID for a school node."""
return f"School_{urn}"
@staticmethod
def get_department_structure_unique_id(school_unique_id: str) -> str:
"""Generate unique ID for a department structure node."""
return f"DepartmentStructure_{school_unique_id}"
@staticmethod
def get_curriculum_structure_unique_id(school_unique_id: str) -> str:
"""Generate unique ID for a curriculum structure node."""
return f"CurriculumStructure_{school_unique_id}"
@staticmethod
def get_pastoral_structure_unique_id(school_unique_id: str) -> str:
"""Generate unique ID for a pastoral structure node."""
return f"PastoralStructure_{school_unique_id}"
@staticmethod
def get_department_unique_id(school_unique_id: str, department_name: str) -> str:
"""Generate unique ID for a department node."""
return f"Department_{school_unique_id}_{department_name.replace(' ', '_')}"
@staticmethod
def get_key_stage_unique_id(curriculum_structure_unique_id: str, key_stage: str) -> str:
"""Generate unique ID for a key stage node."""
return f"KeyStage_{curriculum_structure_unique_id}_KStg{key_stage}"
@staticmethod
def get_year_group_unique_id(school_unique_id: str, year_group: int) -> str:
"""Generate unique ID for a year group node."""
return f"YearGroup_{school_unique_id}_YGrp{year_group}"
@staticmethod
def get_school_path(database_name: str, urn: str) -> str:
"""Generate path for a school node."""
return f"/schools/{database_name}/{urn}"
@staticmethod
def get_department_path(school_path: str, department_name: str) -> str:
"""Generate path for a department node."""
return f"{school_path}/departments/{department_name}"
@staticmethod
def get_department_structure_path(school_path: str) -> str:
"""Generate path for a department structure node."""
return f"{school_path}/departments"
@staticmethod
def get_curriculum_path(school_path: str) -> str:
"""Generate path for a curriculum structure node."""
return f"{school_path}/curriculum"
@staticmethod
def get_pastoral_path(school_path: str) -> str:
"""Generate path for a pastoral structure node."""
return f"{school_path}/pastoral"
@staticmethod
def get_key_stage_path(curriculum_path: str, key_stage: str) -> str:
"""Generate path for a key stage node."""
return f"{curriculum_path}/key_stage_{key_stage}"
@staticmethod
def get_year_group_path(pastoral_path: str, year_group: int) -> str:
"""Generate path for a year group node."""
return f"{pastoral_path}/year_{year_group}"
@staticmethod
def get_cypher_match_school(unique_id: str) -> str:
"""Generate Cypher MATCH clause for finding a school node."""
return f"MATCH (s:{NodeLabels.SCHOOL.value} {{{PropertyKeys.UNIQUE_ID.value}: $school_id}})"
@staticmethod
def get_cypher_check_basic_structure() -> str:
"""Generate Cypher query for checking basic structure existence and validity."""
return """
// Find the school node
MATCH (s:{school})
// Check for department structure with any relationship
OPTIONAL MATCH (s)-[r1]-(dept_struct:{dept_struct})
// Check for curriculum structure with any relationship
OPTIONAL MATCH (s)-[r2]-(curr_struct:{curr_struct})
// Check for pastoral structure with any relationship
OPTIONAL MATCH (s)-[r3]-(past_struct:{past_struct})
// Return structure information
RETURN {{
has_basic:
dept_struct IS NOT NULL AND r1 IS NOT NULL AND
curr_struct IS NOT NULL AND r2 IS NOT NULL AND
past_struct IS NOT NULL AND r3 IS NOT NULL,
department_structure: {{
exists: dept_struct IS NOT NULL AND r1 IS NOT NULL
}},
curriculum_structure: {{
exists: curr_struct IS NOT NULL AND r2 IS NOT NULL
}},
pastoral_structure: {{
exists: past_struct IS NOT NULL AND r3 IS NOT NULL
}}
}} as status
""".format(
school=NodeLabels.SCHOOL.value,
dept_struct=NodeLabels.DEPARTMENT_STRUCTURE.value,
curr_struct=NodeLabels.CURRICULUM_STRUCTURE.value,
past_struct=NodeLabels.PASTORAL_STRUCTURE.value
)
@staticmethod
def get_cypher_check_detailed_structure() -> str:
"""Generate Cypher query for checking detailed structure existence and validity."""
return """
// Find the school node
MATCH (s:{school} {{unique_id: $school_id}})
// Check for department structure and departments
OPTIONAL MATCH (s)-[r1]-(dept_struct:{dept_struct})
WHERE dept_struct.unique_id = 'DepartmentStructure_' + s.unique_id
WITH s, dept_struct, r1,
CASE WHEN dept_struct IS NOT NULL
THEN [(dept_struct)-[r]-(d:{dept}) | d]
ELSE []
END as departments
// Check for curriculum structure and key stages
OPTIONAL MATCH (s)-[r2]-(curr_struct:{curr_struct})
WHERE curr_struct.unique_id = 'CurriculumStructure_' + s.unique_id
WITH s, dept_struct, r1, departments, curr_struct, r2,
CASE WHEN curr_struct IS NOT NULL
THEN [(curr_struct)-[r]-(k:{key_stage}) | k]
ELSE []
END as key_stages
// Check for pastoral structure and year groups
OPTIONAL MATCH (s)-[r3]-(past_struct:{past_struct})
WHERE past_struct.unique_id = 'PastoralStructure_' + s.unique_id
WITH dept_struct, r1, departments, curr_struct, r2, key_stages, past_struct, r3,
CASE WHEN past_struct IS NOT NULL
THEN [(past_struct)-[r]-(y:{year_group}) | y]
ELSE []
END as year_groups
// Return structure information
RETURN {{
has_detailed:
dept_struct IS NOT NULL AND r1 IS NOT NULL AND size(departments) > 0 AND
curr_struct IS NOT NULL AND r2 IS NOT NULL AND size(key_stages) > 0 AND
past_struct IS NOT NULL AND r3 IS NOT NULL AND size(year_groups) > 0,
department_structure: {{
exists: dept_struct IS NOT NULL AND r1 IS NOT NULL,
has_departments: size(departments) > 0,
department_count: size(departments),
node_id: dept_struct.unique_id
}},
curriculum_structure: {{
exists: curr_struct IS NOT NULL AND r2 IS NOT NULL,
has_key_stages: size(key_stages) > 0,
key_stage_count: size(key_stages),
node_id: curr_struct.unique_id
}},
pastoral_structure: {{
exists: past_struct IS NOT NULL AND r3 IS NOT NULL,
has_year_groups: size(year_groups) > 0,
year_group_count: size(year_groups),
node_id: past_struct.unique_id
}}
}} as status
""".format(
school=NodeLabels.SCHOOL.value,
dept_struct=NodeLabels.DEPARTMENT_STRUCTURE.value,
curr_struct=NodeLabels.CURRICULUM_STRUCTURE.value,
past_struct=NodeLabels.PASTORAL_STRUCTURE.value,
dept=NodeLabels.DEPARTMENT.value,
key_stage=NodeLabels.KEY_STAGE.value,
year_group=NodeLabels.YEAR_GROUP.value
)
@staticmethod
def get_schema_definition() -> SchemaDefinition:
"""Get the schema definition instance"""
return SchemaDefinition()
@staticmethod
def get_schema_creation_queries() -> List[str]:
"""Get queries to create the schema"""
return SchemaDefinition.get_schema_creation_queries()
@staticmethod
def get_schema_verification_queries() -> Dict[str, str]:
"""Get queries to verify schema state"""
return SchemaDefinition.get_schema_verification_queries()
@staticmethod
def get_schema_info() -> Dict[str, List[Dict]]:
"""Get human-readable schema information"""
return SchemaDefinition.get_schema_info()
class GraphProvider:
def __init__(self):
"""Initialize the graph provider with Neo4j connection."""
self.neontology = NeontologyProvider()
self.graph_naming = GraphNamingProvider()
self.logger = logging.getLogger(__name__)
def check_schema_status(self, database_name: str) -> Dict[str, Any]:
"""
Checks the current state of the schema in the specified database.
Returns a dictionary containing information about constraints, indexes, and labels.
"""
try:
verification_queries = SchemaDefinition.get_schema_verification_queries()
expected_schema = SchemaDefinition.get_schema_info()
# Get current schema state
constraints = self.neontology.run_query(verification_queries["constraints"], {}, database_name)
indexes = self.neontology.run_query(verification_queries["indexes"], {}, database_name)
labels = self.neontology.run_query(verification_queries["labels"], {}, database_name)
# Process results
current_constraints = [c["name"] for c in constraints]
current_indexes = [i["name"] for i in indexes]
current_labels = [l["label"] for l in labels]
# Expected values
expected_labels = [node["label"] for node in expected_schema["nodes"]]
return {
"constraints": current_constraints,
"constraints_valid": len(current_constraints) >= 4, # We expect at least 4 unique constraints
"indexes": current_indexes,
"indexes_valid": len(current_indexes) >= 5, # We expect at least 5 indexes
"labels": current_labels,
"labels_valid": all(label in current_labels for label in expected_labels)
}
except Exception as e:
self.logger.error(f"Error checking schema status: {str(e)}")
return {
"constraints": [], "constraints_valid": False,
"indexes": [], "indexes_valid": False,
"labels": [], "labels_valid": False
}
def initialize_schema(self, database_name: str) -> None:
"""
Initializes the schema for the specified database by creating all necessary
constraints and indexes.
"""
try:
creation_queries = SchemaDefinition.get_schema_creation_queries()
for query in creation_queries:
self.neontology.cypher_write(query, {}, database_name)
self.logger.info(f"Schema initialized successfully for database {database_name}")
except Exception as e:
self.logger.error(f"Error initializing schema: {str(e)}")
raise
def get_schema_info(self) -> Dict[str, Any]:
"""
Returns the schema definition information.
"""
return SchemaDefinition.get_schema_info()
@@ -0,0 +1,212 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
from modules.database.tools.neontology.graphconnection import GraphConnection, init_neontology
from modules.database.tools.neontology.basenode import BaseNode
from modules.database.tools.neontology.baserelationship import BaseRelationship
from typing import Optional, Dict, Any, List
from neo4j import Record as Neo4jRecord
import re
log_name = 'api_modules_database_admin_neontology_provider'
log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback
logging = logger.get_logger(
name=log_name,
log_level=os.getenv("LOG_LEVEL", "DEBUG"),
log_path=log_dir,
log_file=log_name,
runtime=True,
log_format='default'
)
class NeontologyProvider:
"""Provider class for managing Neontology connections and operations."""
def __init__(self):
"""Initialize the provider with Neo4j connection details from environment."""
self.bolt_url = os.getenv("APP_BOLT_URL")
self.user = os.getenv("USER_NEO4J")
self.password = os.getenv("PASSWORD_NEO4J")
self.connection = None
self.current_database = None
def _validate_database_name(self, database: str) -> str:
"""
Validate and format database name to handle special characters.
Args:
database: The database name to validate
Returns:
str: The validated database name
Raises:
ValueError: If database name is invalid
"""
if not database:
raise ValueError("Database name cannot be empty")
# Check for valid database name pattern
# Allow letters, numbers, underscores, and dots
if not re.match(r'^[a-zA-Z0-9_\.]+$', database):
raise ValueError("Database name contains invalid characters")
# For database names with multiple dots, we need to handle them specially
# Neo4j treats dots as special characters in some contexts
if database.count('.') > 1:
# Replace dots with underscores except for the first one
parts = database.split('.')
if len(parts) > 2:
# Keep the first dot, replace others with underscore
formatted_name = f"{parts[0]}.{'.'.join(parts[1:])}"
logging.info(f"Reformatted database name from {database} to {formatted_name}")
return formatted_name
return database
def connect(self, database: str = 'neo4j') -> None:
"""Establish connection to Neo4j using Neontology."""
try:
# Validate and format database name
formatted_database = self._validate_database_name(database)
# If we're switching databases, ensure we close the old connection
if self.current_database != formatted_database and self.connection is not None:
self.close()
# Initialize Neontology connection if needed
if self.connection is None:
init_neontology(
neo4j_uri=self.bolt_url,
neo4j_username=self.user,
neo4j_password=self.password
)
# Get the GraphConnection instance
self.connection = GraphConnection()
self.current_database = formatted_database
logging.info(f"Neontology connection initialized with host: {self.host}, port: {self.port}, database: {formatted_database}")
except Exception as e:
logging.error(f"Failed to initialize Neontology connection: {str(e)}")
raise
def reset_connection(self) -> None:
"""Reset the connection, forcing a new one to be created on next use."""
if self.connection:
self.close()
def create_or_merge_node(self, node: BaseNode, database: str = 'neo4j', operation: str = "merge") -> None:
"""Create or merge a node in the Neo4j database."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
if operation == "create":
node.create(database=database)
elif operation == "merge":
node.merge(database=database)
else:
logging.error(f"Invalid operation: {operation}")
raise ValueError(f"Invalid operation: {operation}")
except Exception as e:
logging.error(f"Error in processing node: {e}")
raise
def create_or_merge_relationship(self, relationship: BaseRelationship, database: str = 'neo4j', operation: str = "merge") -> None:
"""Create or merge a relationship in the Neo4j database."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
if operation == "create":
relationship.create(database=database)
elif operation == "merge":
relationship.merge(database=database)
else:
logging.error(f"Invalid operation: {operation}")
raise ValueError(f"Invalid operation: {operation}")
except Exception as e:
logging.error(f"Error in processing relationship: {e}")
raise
def cypher_write(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> None:
"""Execute a write transaction."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
self.connection.cypher_write(cypher, params)
except Exception as e:
logging.error(f"Error in cypher write: {e}")
raise
def cypher_read(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> Optional[Neo4jRecord]:
"""Execute a read transaction returning a single record."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
return self.connection.cypher_read(cypher, params)
except Exception as e:
logging.error(f"Error in cypher read: {e}")
raise
def cypher_read_many(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Neo4jRecord]:
"""Execute a read transaction returning multiple records."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
return self.connection.cypher_read_many(cypher, params)
except Exception as e:
logging.error(f"Error in cypher read many: {e}")
raise
def run_query(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Dict[str, Any]]:
"""
Execute a Cypher query and return results as a list of dictionaries.
This is a convenience method that handles both single and multiple record results.
Args:
cypher: The Cypher query to execute
params: Query parameters
database: Target database name
Returns:
List[Dict[str, Any]]: Query results as a list of dictionaries
"""
try:
if not self.connection or self.current_database != database:
self.connect(database)
# Use cypher_read_many for consistent return type
records = self.connection.cypher_read_many(cypher, params)
# Convert Neo4j records to dictionaries
results = []
for record in records:
# Handle both Record and dict types
if isinstance(record, Neo4jRecord):
results.append(dict(record))
else:
results.append(record)
return results
except Exception as e:
logging.error(f"Error in run_query: {e}")
raise
def close(self) -> None:
"""Close the Neontology connection."""
if self.connection:
# The connection will be closed when the GraphConnection instance is deleted
self.connection = None
self.current_database = None
logging.info("Neontology connection closed")
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.close()
@@ -0,0 +1,797 @@
import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
import backend.modules.database.schemas.entities as neo_entity
import modules.database.schemas.curriculum_neo as neo_curriculum
import modules.database.schemas.relationships.curriculum_relationships as curriculum_relationships
import modules.database.schemas.relationships.entity_relationships as ent_rels
import modules.database.schemas.relationships.entity_curriculum_rels as ent_cur_rels
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
import modules.database.tools.neontology_tools as neon
import pandas as pd
# Default values for nodes
default_topic_values = {
'topic_assessment_type': 'Null',
'topic_type': 'Null',
'total_number_of_lessons_for_topic': '1',
'topic_title': 'Null'
}
default_topic_lesson_values = {
'topic_lesson_title': 'Null',
'topic_lesson_type': 'Null',
'topic_lesson_length': '1',
'topic_lesson_suggested_activities': 'Null',
'topic_lesson_skills_learned': 'Null',
'topic_lesson_weblinks': 'Null',
}
default_learning_statement_values = {
'lesson_learning_statement': 'Null',
'lesson_learning_statement_type': 'Student learning outcome'
}
# Helper function to sort year groups numerically where possible
def sort_year_groups(df):
df = df.copy()
df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce')
return df.sort_values(by='YearGroupNumeric')
def create_curriculum(dataframes, school_db_name, curriculum_db_name, school_node):
fs_handler = ClassroomCopilotFilesystem(school_db_name, init_run_type="school")
logger.info(f"Initialising neo4j connection...")
neon.init_neontology_connection()
keystagesyllabus_df = dataframes['keystagesyllabuses']
yeargroupsyllabus_df = dataframes['yeargroupsyllabuses']
topic_df = dataframes['topics']
lesson_df = dataframes['lessons']
statement_df = dataframes['statements']
# resource_df = dataframes['resources'] # TODO
node_library = {}
node_library['key_stage_nodes'] = {}
node_library['year_group_nodes'] = {}
node_library['key_stage_syllabus_nodes'] = {}
node_library['year_group_syllabus_nodes'] = {}
node_library['topic_nodes'] = {}
node_library['topic_lesson_nodes'] = {}
node_library['statement_nodes'] = {}
node_library['department_nodes'] = {}
node_library['subject_nodes'] = {}
curriculum_node = None
pastoral_node = None
key_stage_nodes_created = {}
year_group_nodes_created = {}
last_year_group_node = None
last_key_stage_node = None
# Create Curriculum and Pastoral nodes and relationships with School in both databases
_, curriculum_path = fs_handler.create_school_curriculum_directory(school_node.path)
_, pastoral_path = fs_handler.create_school_pastoral_directory(school_node.path)
# Create Department Structure node
department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}"
department_structure_node = neo_entity.DepartmentStructureNode(
unique_id=department_structure_node_unique_id,
path=os.path.join(school_node.path, "departments")
)
# Create in school database only
neon.create_or_merge_neontology_node(department_structure_node, database=school_db_name, operation='merge')
fs_handler.create_default_tldraw_file(department_structure_node.path, department_structure_node.to_dict())
node_library['department_structure_node'] = department_structure_node
# Link Department Structure to School
neon.create_or_merge_neontology_relationship(
ent_rels.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created department structure node and linked to school")
# Create Curriculum Structure node
curriculum_structure_node_unique_id = f"CurriculumStructure_{school_node.unique_id}"
curriculum_node = neo_curriculum.CurriculumStructureNode(
unique_id=curriculum_structure_node_unique_id,
path=curriculum_path
)
# Create in school database only
neon.create_or_merge_neontology_node(curriculum_node, database=school_db_name, operation='merge')
fs_handler.create_default_tldraw_file(curriculum_node.path, curriculum_node.to_dict())
node_library['curriculum_node'] = curriculum_node
# Create relationship in school database only
neon.create_or_merge_neontology_relationship(
ent_cur_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created curriculum node and relationship with school")
# Create Pastoral Structure node
pastoral_structure_node_unique_id = f"PastoralStructure_{school_node.unique_id}"
pastoral_node = neo_curriculum.PastoralStructureNode(
unique_id=pastoral_structure_node_unique_id,
path=pastoral_path
)
neon.create_or_merge_neontology_node(pastoral_node, database=school_db_name, operation='merge')
fs_handler.create_default_tldraw_file(pastoral_node.path, pastoral_node.to_dict())
node_library['pastoral_node'] = pastoral_node
neon.create_or_merge_neontology_relationship(
ent_cur_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created pastoral node and relationship with school")
# Create departments and subjects
# First get unique departments
unique_departments = keystagesyllabus_df['Department'].dropna().unique()
for department_name in unique_departments:
department_unique_id = f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}"
_, department_path = fs_handler.create_school_department_directory(school_node.path, department_name)
department_node = neo_entity.DepartmentNode(
unique_id=department_unique_id,
department_name=department_name,
path=department_path
)
# Create department in school database only
neon.create_or_merge_neontology_node(department_node, database=school_db_name, operation='merge')
fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict())
node_library['department_nodes'][department_name] = department_node
# Link department to department structure in school database
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created department node for {department_name} and linked to department structure")
# Create subjects and link to departments
# First get unique subjects from key stage syllabuses (which have department info)
unique_subjects = keystagesyllabus_df[['Subject', 'SubjectCode', 'Department']].drop_duplicates()
# Then add any additional subjects from year group syllabuses (without department info)
additional_subjects = yeargroupsyllabus_df[['Subject', 'SubjectCode']].drop_duplicates()
additional_subjects = additional_subjects[~additional_subjects['SubjectCode'].isin(unique_subjects['SubjectCode'])]
# Process subjects from key stage syllabuses first (these have department info)
for _, subject_row in unique_subjects.iterrows():
subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}"
department_node = node_library['department_nodes'].get(subject_row['Department'])
if not department_node:
logger.warning(f"No department found for subject {subject_row['Subject']} with code {subject_row['SubjectCode']}")
continue
_, subject_path = fs_handler.create_department_subject_directory(
department_node.path,
subject_row['Subject'] # Use full subject name instead of SubjectCode
)
logger.info(f"Created subject directory for {subject_path}")
subject_node = neo_curriculum.SubjectNode(
unique_id=subject_unique_id,
subject_code=subject_row['SubjectCode'],
subject_name=subject_row['Subject'],
path=subject_path
)
# Create subject in both databases
neon.create_or_merge_neontology_node(subject_node, database=school_db_name, operation='merge')
neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(subject_node.path, subject_node.to_dict())
node_library['subject_nodes'][subject_row['Subject']] = subject_node
# Link subject to department in school database only
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentManagesSubject(source=department_node, target=subject_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created subject node for {subject_row['Subject']} and linked to department {subject_row['Department']}")
# Process any additional subjects from year group syllabuses (these won't have department info)
for _, subject_row in additional_subjects.iterrows():
subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}"
# Create in a special "Unassigned" department
unassigned_dept_name = "Unassigned Department"
if unassigned_dept_name not in node_library['department_nodes']:
_, dept_path = fs_handler.create_school_department_directory(school_node.path, unassigned_dept_name)
department_node = neo_entity.DepartmentNode(
unique_id=f"Department_{school_node.unique_id}_Unassigned",
department_name=unassigned_dept_name,
path=dept_path
)
neon.create_or_merge_neontology_node(department_node, database=school_db_name, operation='merge')
fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict())
node_library['department_nodes'][unassigned_dept_name] = department_node
# Link unassigned department to department structure
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created unassigned department node and linked to department structure")
_, subject_path = fs_handler.create_department_subject_directory(
node_library['department_nodes'][unassigned_dept_name].path,
subject_row['Subject']
)
subject_node = neo_curriculum.SubjectNode(
unique_id=subject_unique_id,
subject_code=subject_row['SubjectCode'],
subject_name=subject_row['Subject'],
path=subject_path
)
# Create subject in both databases
neon.create_or_merge_neontology_node(subject_node, database=school_db_name, operation='merge')
neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(subject_node.path, subject_node.to_dict())
node_library['subject_nodes'][subject_row['Subject']] = subject_node
# Link subject to unassigned department in school database only
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentManagesSubject(
source=node_library['department_nodes'][unassigned_dept_name],
target=subject_node
),
database=school_db_name, operation='merge'
)
logger.warning(f"Created subject node for {subject_row['Subject']} in unassigned department")
# Process key stages and syllabuses
logger.info(f"Processing key stages")
last_key_stage_node = None
# Track last syllabus nodes per subject
last_key_stage_syllabus_nodes = {} # Dictionary to track last key stage syllabus node per subject
last_year_group_syllabus_nodes = {} # Dictionary to track last year group syllabus node per subject
topics_processed = set() # Track which topics have been processed
lessons_processed = set() # Track which lessons have been processed
statements_processed = set() # Track which statements have been processed
# First create all key stage nodes and key stage syllabus nodes
for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows():
key_stage = str(ks_row['KeyStage'])
logger.debug(f"Processing key stage syllabus row - Subject: {ks_row['Subject']}, Key Stage: {key_stage}")
subject_node = node_library['subject_nodes'].get(ks_row['Subject'])
if not subject_node:
logger.warning(f"No subject node found for subject {ks_row['Subject']}")
continue
if key_stage not in key_stage_nodes_created:
key_stage_node_unique_id = f"KeyStage_{curriculum_node.unique_id}_KStg{key_stage}"
key_stage_node = neo_curriculum.KeyStageNode(
unique_id=key_stage_node_unique_id,
key_stage_name=f"Key Stage {key_stage}",
key_stage=str(key_stage),
path=os.path.join(curriculum_node.path, "key_stages", f"KS{key_stage}")
)
# Create key stage node in both databases
neon.create_or_merge_neontology_node(key_stage_node, database=school_db_name, operation='merge')
neon.create_or_merge_neontology_node(key_stage_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(key_stage_node.path, key_stage_node.to_dict())
key_stage_nodes_created[key_stage] = key_stage_node
node_library['key_stage_nodes'][key_stage] = key_stage_node
# Create relationship with curriculum structure in school database only
neon.create_or_merge_neontology_relationship(
curriculum_relationships.CurriculumStructureIncludesKeyStage(source=curriculum_node, target=key_stage_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created key stage node {key_stage_node_unique_id} and relationship with curriculum structure")
# Create sequential relationship between key stages in both databases
if last_key_stage_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between key stages {last_key_stage_node.unique_id} and {key_stage_node.unique_id}")
last_key_stage_node = key_stage_node
# Create key stage syllabus under the subject's curriculum directory
_, key_stage_syllabus_path = fs_handler.create_curriculum_key_stage_syllabus_directory(
curriculum_node.path,
key_stage,
ks_row['Subject'],
ks_row['ID']
)
logger.debug(f"Creating key stage syllabus node for {ks_row['Subject']} KS{key_stage} with ID {ks_row['ID']}")
key_stage_syllabus_node_unique_id = f"KeyStageSyllabus_{curriculum_node.unique_id}_{ks_row['Title'].replace(' ', '')}"
key_stage_syllabus_node = neo_curriculum.KeyStageSyllabusNode(
unique_id=key_stage_syllabus_node_unique_id,
ks_syllabus_id=ks_row['ID'],
ks_syllabus_name=ks_row['Title'],
ks_syllabus_key_stage=str(ks_row['KeyStage']),
ks_syllabus_subject=ks_row['Subject'],
ks_syllabus_subject_code=ks_row['Subject'],
path=key_stage_syllabus_path
)
# Create key stage syllabus node in both databases
neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=school_db_name, operation='merge')
neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(key_stage_syllabus_node.path, key_stage_syllabus_node.to_dict())
node_library['key_stage_syllabus_nodes'][ks_row['ID']] = key_stage_syllabus_node
logger.debug(f"Created key stage syllabus node {key_stage_syllabus_node_unique_id} for {ks_row['Subject']} KS{key_stage}")
# Link key stage syllabus to its subject in both databases
if subject_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between subject {subject_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}")
# Link key stage syllabus to its key stage in both databases
key_stage_node = key_stage_nodes_created.get(key_stage)
if key_stage_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between key stage {key_stage_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}")
# Create sequential relationship between key stage syllabuses in both databases
last_key_stage_syllabus_node = last_key_stage_syllabus_nodes.get(ks_row['Subject'])
if last_key_stage_syllabus_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between key stage syllabuses {last_key_stage_syllabus_node.unique_id} and {key_stage_syllabus_node.unique_id}")
last_key_stage_syllabus_nodes[ks_row['Subject']] = key_stage_syllabus_node
# Now process year groups and their syllabuses
for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows():
key_stage = str(ks_row['KeyStage'])
related_yeargroups = sort_year_groups(yeargroupsyllabus_df[yeargroupsyllabus_df['KeyStage'] == ks_row['KeyStage']])
logger.info(f"Processing year groups for key stage {key_stage}")
for yg_index, yg_row in related_yeargroups.iterrows():
year_group = yg_row['YearGroup']
subject_code = yg_row['SubjectCode']
numeric_year_group = pd.to_numeric(year_group, errors='coerce')
if pd.notna(numeric_year_group):
numeric_year_group = int(numeric_year_group)
if numeric_year_group not in year_group_nodes_created:
# Create year group directory under pastoral structure
_, year_group_path = fs_handler.create_pastoral_year_group_directory(pastoral_node.path, year_group)
logger.info(f"Created year group directory for {year_group_path}")
year_group_node_unique_id = f"YearGroup_{school_node.unique_id}_YGrp{numeric_year_group}"
year_group_node = neo_curriculum.YearGroupNode(
unique_id=year_group_node_unique_id,
year_group=str(numeric_year_group),
year_group_name=f"Year {numeric_year_group}",
path=year_group_path
)
# Create year group node in both databases but use same directory
neon.create_or_merge_neontology_node(year_group_node, database=school_db_name, operation='merge')
neon.create_or_merge_neontology_node(year_group_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(year_group_node.path, year_group_node.to_dict())
# Create sequential relationship between year groups in both databases
if last_year_group_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between year groups {last_year_group_node.unique_id} and {year_group_node.unique_id} across key stages")
last_year_group_node = year_group_node
# Create relationship with Pastoral Structure in school database only
neon.create_or_merge_neontology_relationship(
curriculum_relationships.PastoralStructureIncludesYearGroup(source=pastoral_node, target=year_group_node),
database=school_db_name, operation='merge'
)
logger.info(f"Created year group node {year_group_node_unique_id} and relationship with pastoral structure")
year_group_nodes_created[numeric_year_group] = year_group_node
node_library['year_group_nodes'][str(numeric_year_group)] = year_group_node
# Curriculum specific database initialisation begins here
# Create year group syllabus nodes in both databases
year_group_node = year_group_nodes_created.get(numeric_year_group)
if year_group_node:
# Create syllabus directory under curriculum structure
_, year_group_syllabus_path = fs_handler.create_curriculum_year_group_syllabus_directory(
curriculum_node.path,
yg_row['Subject'],
year_group,
yg_row['ID']
)
logger.info(f"Created year group syllabus directory for {year_group_syllabus_path}")
year_group_syllabus_node_unique_id = f"YearGroupSyllabus_{school_node.unique_id}_{yg_row['ID']}"
year_group_syllabus_node = neo_curriculum.YearGroupSyllabusNode(
unique_id=year_group_syllabus_node_unique_id,
yr_syllabus_id=yg_row['ID'],
yr_syllabus_name=yg_row['Title'],
yr_syllabus_year_group=str(yg_row['YearGroup']),
yr_syllabus_subject=yg_row['Subject'],
yr_syllabus_subject_code=yg_row['Subject'],
path=year_group_syllabus_path
)
# Create year group syllabus node in both databases but use same directory
neon.create_or_merge_neontology_node(year_group_syllabus_node, database=school_db_name, operation='merge')
neon.create_or_merge_neontology_node(year_group_syllabus_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(year_group_syllabus_node.path, year_group_syllabus_node.to_dict())
node_library['year_group_syllabus_nodes'][yg_row['ID']] = year_group_syllabus_node
# Create sequential relationship between year group syllabuses in both databases
last_year_group_syllabus_node = last_year_group_syllabus_nodes.get(yg_row['Subject'])
# Only create sequential relationship if this year group is higher than the last one
if last_year_group_syllabus_node:
last_year = pd.to_numeric(last_year_group_syllabus_node.yr_syllabus_year_group, errors='coerce')
current_year = pd.to_numeric(year_group_syllabus_node.yr_syllabus_year_group, errors='coerce')
if pd.notna(last_year) and pd.notna(current_year) and current_year > last_year:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between year group syllabuses {last_year_group_syllabus_node.unique_id} and {year_group_syllabus_node.unique_id}")
last_year_group_syllabus_nodes[yg_row['Subject']] = year_group_syllabus_node
# Create relationships in both databases using MATCH to avoid cartesian products
subject_node = node_library['subject_nodes'].get(yg_row['Subject'])
if subject_node:
# Link to subject
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between subject {subject_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Link to year group
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between year group {year_group_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Link to key stage syllabus if it exists for the same subject
key_stage_syllabus_node = node_library['key_stage_syllabus_nodes'].get(ks_row['ID'])
if key_stage_syllabus_node and yg_row['Subject'] == ks_row['Subject']:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node),
database=school_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between key stage syllabus {key_stage_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Processing of curriculum topic begins here
# Process topics for this year group syllabus only if not already processed
topics_for_syllabus = topic_df[topic_df['SyllabusYearID'] == yg_row['ID']]
for _, topic_row in topics_for_syllabus.iterrows():
if topic_row['TopicID'] in topics_processed:
continue
topics_processed.add(topic_row['TopicID'])
# Get the correct subject from the topic row
topic_subject = topic_row['SyllabusSubject']
topic_key_stage = topic_row['SyllabusKeyStage']
logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage}")
logger.debug(f"Available key stage syllabus nodes: {[node.ks_syllabus_subject + '_KS' + node.ks_syllabus_key_stage for node in node_library['key_stage_syllabus_nodes'].values()]}")
# Find the key stage syllabus node by iterating through all nodes
matching_syllabus_node = None
for syllabus_node in node_library['key_stage_syllabus_nodes'].values():
logger.debug(f"Checking syllabus node - Subject: {syllabus_node.ks_syllabus_subject}, Key Stage: {syllabus_node.ks_syllabus_key_stage}")
logger.debug(f"Comparing with - Subject: {topic_subject}, Key Stage: {str(topic_key_stage)}")
logger.debug(f"Types - Node Subject: {type(syllabus_node.ks_syllabus_subject)}, Topic Subject: {type(topic_subject)}")
logger.debug(f"Types - Node Key Stage: {type(syllabus_node.ks_syllabus_key_stage)}, Topic Key Stage: {type(str(topic_key_stage))}")
if (syllabus_node.ks_syllabus_subject == topic_subject and
syllabus_node.ks_syllabus_key_stage == str(topic_key_stage)):
matching_syllabus_node = syllabus_node
logger.debug(f"Found matching syllabus node: {syllabus_node.unique_id}")
break
if not matching_syllabus_node:
logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation")
continue
_, topic_path = fs_handler.create_curriculum_topic_directory(matching_syllabus_node.path, topic_row['TopicID'])
logger.info(f"Created topic directory for {topic_path}")
topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}"
topic_node = neo_curriculum.TopicNode(
unique_id=topic_node_unique_id,
topic_id=topic_row['TopicID'],
topic_title=topic_row.get('TopicTitle', default_topic_values['topic_title']),
total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])),
topic_type=topic_row.get('TopicType', default_topic_values['topic_type']),
topic_assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']),
path=topic_path
)
# Create topic node in curriculum database only
neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(topic_node.path, topic_node.to_dict())
node_library['topic_nodes'][topic_row['TopicID']] = topic_node
# Link topic to key stage syllabus as well as year group syllabus
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node),
database=curriculum_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupSyllabusIncludesTopic(source=year_group_syllabus_node, target=topic_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationships between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Process lessons for this topic only if not already processed
lessons_for_topic = lesson_df[
(lesson_df['TopicID'] == topic_row['TopicID']) &
(lesson_df['SyllabusSubject'] == topic_subject)
].copy()
lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str)
lessons_for_topic = lessons_for_topic.sort_values('Lesson')
previous_lesson_node = None
for _, lesson_row in lessons_for_topic.iterrows():
if lesson_row['LessonID'] in lessons_processed:
continue
lessons_processed.add(lesson_row['LessonID'])
_, lesson_path = fs_handler.create_curriculum_lesson_directory(topic_path, lesson_row['LessonID'])
logger.info(f"Created lesson directory for {lesson_path}")
lesson_data = {
'unique_id': f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}",
'topic_lesson_id': lesson_row['LessonID'],
'topic_lesson_title': lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']),
'topic_lesson_type': lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']),
'topic_lesson_length': str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])),
'topic_lesson_suggested_activities': lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities']),
'topic_lesson_skills_learned': lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned']),
'topic_lesson_weblinks': lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks']),
'path': lesson_path
}
for key, value in lesson_data.items():
if pd.isna(value):
lesson_data[key] = default_topic_lesson_values.get(key, 'Null')
lesson_node = neo_curriculum.TopicLessonNode(**lesson_data)
# Create lesson node in curriculum database only
neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(lesson_node.path, lesson_node.to_dict())
node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node
# Link lesson to topic
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}")
# Create sequential relationships between lessons
if lesson_row['Lesson'].isdigit() and previous_lesson_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}")
previous_lesson_node = lesson_node
# Process learning statements for this lesson only if not already processed
statements_for_lesson = statement_df[
(statement_df['LessonID'] == lesson_row['LessonID']) &
(statement_df['SyllabusSubject'] == topic_subject)
]
for _, statement_row in statements_for_lesson.iterrows():
if statement_row['StatementID'] in statements_processed:
continue
statements_processed.add(statement_row['StatementID'])
_, statement_path = fs_handler.create_curriculum_learning_statement_directory(lesson_path, statement_row['StatementID'])
statement_data = {
'unique_id': f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}",
'lesson_learning_statement_id': statement_row['StatementID'],
'lesson_learning_statement': statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']),
'lesson_learning_statement_type': statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']),
'path': statement_path
}
for key in statement_data:
if pd.isna(statement_data[key]):
statement_data[key] = default_learning_statement_values.get(key, 'Null')
statement_node = neo_curriculum.LearningStatementNode(**statement_data)
# Create statement node in curriculum database only
neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(statement_node.path, statement_node.to_dict())
node_library['statement_nodes'][statement_row['StatementID']] = statement_node
# Link learning statement to lesson
neon.create_or_merge_neontology_relationship(
curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}")
else:
logger.warning(f"No year group node found for year group {year_group}, skipping syllabus creation")
# After processing all year groups and their syllabuses, process any remaining topics
logger.info("Processing topics without year groups")
for _, topic_row in topic_df.iterrows():
if topic_row['TopicID'] in topics_processed:
continue
topic_subject = topic_row['SyllabusSubject']
topic_key_stage = topic_row['SyllabusKeyStage']
logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage} without year group")
# Find the key stage syllabus node
matching_syllabus_node = None
for syllabus_node in node_library['key_stage_syllabus_nodes'].values():
if (syllabus_node.ks_syllabus_subject == topic_subject and
syllabus_node.ks_syllabus_key_stage == str(topic_key_stage)):
matching_syllabus_node = syllabus_node
break
if not matching_syllabus_node:
logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation")
continue
_, topic_path = fs_handler.create_curriculum_topic_directory(matching_syllabus_node.path, topic_row['TopicID'])
logger.info(f"Created topic directory for {topic_path}")
topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}"
topic_node = neo_curriculum.TopicNode(
unique_id=topic_node_unique_id,
topic_id=topic_row['TopicID'],
topic_title=topic_row.get('TopicTitle', default_topic_values['topic_title']),
total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])),
topic_type=topic_row.get('TopicType', default_topic_values['topic_type']),
topic_assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']),
path=topic_path
)
# Create topic node in curriculum database only
neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(topic_node.path, topic_node.to_dict())
node_library['topic_nodes'][topic_row['TopicID']] = topic_node
topics_processed.add(topic_row['TopicID'])
# Link topic to key stage syllabus
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id}")
# Process lessons for this topic
lessons_for_topic = lesson_df[
(lesson_df['TopicID'] == topic_row['TopicID']) &
(lesson_df['SyllabusSubject'] == topic_subject)
].copy()
lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str)
lessons_for_topic = lessons_for_topic.sort_values('Lesson')
previous_lesson_node = None
for _, lesson_row in lessons_for_topic.iterrows():
if lesson_row['LessonID'] in lessons_processed:
continue
lessons_processed.add(lesson_row['LessonID'])
_, lesson_path = fs_handler.create_curriculum_lesson_directory(topic_path, lesson_row['LessonID'])
logger.info(f"Created lesson directory for {lesson_path}")
lesson_data = {
'unique_id': f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}",
'topic_lesson_id': lesson_row['LessonID'],
'topic_lesson_title': lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']),
'topic_lesson_type': lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']),
'topic_lesson_length': str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])),
'topic_lesson_suggested_activities': lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities']),
'topic_lesson_skills_learned': lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned']),
'topic_lesson_weblinks': lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks']),
'path': lesson_path
}
for key, value in lesson_data.items():
if pd.isna(value):
lesson_data[key] = default_topic_lesson_values.get(key, 'Null')
lesson_node = neo_curriculum.TopicLessonNode(**lesson_data)
# Create lesson node in curriculum database only
neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(lesson_node.path, lesson_node.to_dict())
node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node
# Link lesson to topic
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}")
# Create sequential relationships between lessons
if lesson_row['Lesson'].isdigit() and previous_lesson_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}")
previous_lesson_node = lesson_node
# Process learning statements for this lesson
statements_for_lesson = statement_df[
(statement_df['LessonID'] == lesson_row['LessonID']) &
(statement_df['SyllabusSubject'] == topic_subject)
]
for _, statement_row in statements_for_lesson.iterrows():
if statement_row['StatementID'] in statements_processed:
continue
statements_processed.add(statement_row['StatementID'])
_, statement_path = fs_handler.create_curriculum_learning_statement_directory(lesson_path, statement_row['StatementID'])
statement_data = {
'unique_id': f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}",
'lesson_learning_statement_id': statement_row['StatementID'],
'lesson_learning_statement': statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']),
'lesson_learning_statement_type': statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']),
'path': statement_path
}
for key in statement_data:
if pd.isna(statement_data[key]):
statement_data[key] = default_learning_statement_values.get(key, 'Null')
statement_node = neo_curriculum.LearningStatementNode(**statement_data)
# Create statement node in curriculum database only
neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge')
fs_handler.create_default_tldraw_file(statement_node.path, statement_node.to_dict())
node_library['statement_nodes'][statement_row['StatementID']] = statement_node
# Link learning statement to lesson
neon.create_or_merge_neontology_relationship(
curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}")
return node_library
+512
View File
@@ -0,0 +1,512 @@
import os
from modules.logger_tool import initialise_logger
from supabase import create_client
import json
import pandas as pd
import modules.database.init.xl_tools as xl
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
import modules.database.tools.neo4j_driver_tools as driver_tools
import modules.database.tools.neo4j_session_tools as session_tools
import modules.database.schemas.nodes.schools.schools as school_schemas
import modules.database.schemas.nodes.schools.curriculum as curriculum_schemas
import modules.database.schemas.nodes.schools.pastoral as pastoral_schemas
import modules.database.schemas.nodes.structures.schools as school_structures
import modules.database.schemas.entities as entities
from modules.database.admin.neontology_provider import NeontologyProvider
from modules.database.admin.graph_provider import GraphNamingProvider
from modules.database.schemas.relationships import curriculum_relationships, entity_relationships, entity_curriculum_rels
class SchoolManager:
def __init__(self):
self.driver = driver_tools.get_driver()
self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
self.neontology = NeontologyProvider()
self.graph_naming = GraphNamingProvider()
# Initialize Supabase client with correct URL and service role key
supabase_url = os.getenv("SUPABASE_URL")
service_role_key = os.getenv("SERVICE_ROLE_KEY")
self.logger.info(f"Initializing Supabase client with URL: {supabase_url}")
self.supabase = create_client(supabase_url, service_role_key)
# Set headers for admin operations
self.supabase.headers = {
"apiKey": service_role_key,
"Authorization": f"Bearer {service_role_key}",
"Content-Type": "application/json"
}
# Set storage client headers explicitly
self.supabase.storage._client.headers.update({
"apiKey": service_role_key,
"Authorization": f"Bearer {service_role_key}",
"Content-Type": "application/json"
})
def create_schools_database(self):
"""Creates the main cc.institutes database in Neo4j"""
try:
db_name = "cc.institutes"
with self.driver.session() as session:
return self._extracted_from_create_private_database(
session, db_name, f'Created database {db_name}'
)
except Exception as e:
self.logger.error(f"Error creating schools database: {str(e)}")
return {"status": "error", "message": str(e)}
def create_school_node(self, school_data):
"""Creates a school node in cc.institutes database and stores TLDraw file in Supabase"""
try:
# Convert Supabase school data to SchoolNode using GraphNamingProvider
school_unique_id = self.graph_naming.get_school_unique_id(school_data['urn'])
school_path = self.graph_naming.get_school_path("cc.institutes", school_data['urn'])
school_node = entities.school_schemas.SchoolNode(
unique_id=school_unique_id,
path=school_path,
urn=school_data['urn'],
establishment_number=school_data['establishment_number'],
establishment_name=school_data['establishment_name'],
establishment_type=school_data['establishment_type'],
establishment_status=school_data['establishment_status'],
phase_of_education=school_data['phase_of_education'] if school_data['phase_of_education'] not in [None, ''] else None,
statutory_low_age=int(school_data['statutory_low_age']) if school_data.get('statutory_low_age') is not None else 0,
statutory_high_age=int(school_data['statutory_high_age']) if school_data.get('statutory_high_age') is not None else 0,
religious_character=school_data.get('religious_character') if school_data.get('religious_character') not in [None, ''] else None,
school_capacity=int(school_data['school_capacity']) if school_data.get('school_capacity') is not None else 0,
school_website=school_data.get('school_website', ''),
ofsted_rating=school_data.get('ofsted_rating') if school_data.get('ofsted_rating') not in [None, ''] else None
)
# Create default tldraw file data
tldraw_data = {
"document": {
"version": 1,
"id": school_data['urn'],
"name": school_data['establishment_name'],
"meta": {
"created_at": "",
"updated_at": "",
"creator_id": "",
"is_template": False,
"is_snapshot": False,
"is_draft": False,
"template_id": None,
"snapshot_id": None,
"draft_id": None
}
},
"schema": {
"schemaVersion": 1,
"storeVersion": 4,
"recordVersions": {
"asset": {
"version": 1,
"subTypeKey": "type",
"subTypeVersions": {}
},
"camera": {
"version": 1
},
"document": {
"version": 2
},
"instance": {
"version": 22
},
"instance_page_state": {
"version": 5
},
"page": {
"version": 1
},
"shape": {
"version": 3,
"subTypeKey": "type",
"subTypeVersions": {
"cc-school-node": 1
}
},
"instance_presence": {
"version": 5
},
"pointer": {
"version": 1
}
}
},
"store": {
"document:document": {
"gridSize": 10,
"name": school_data['establishment_name'],
"meta": {},
"id": school_data['urn'],
"typeName": "document"
},
"page:page": {
"meta": {},
"id": "page",
"name": "Page 1",
"index": "a1",
"typeName": "page"
},
"shape:school-node": {
"x": 0,
"y": 0,
"rotation": 0,
"type": "cc-school-node",
"id": school_unique_id,
"parentId": "page",
"index": "a1",
"props": school_node.to_dict(),
"typeName": "shape"
},
"instance:instance": {
"id": "instance",
"currentPageId": "page",
"typeName": "instance"
},
"camera:camera": {
"x": 0,
"y": 0,
"z": 1,
"id": "camera",
"typeName": "camera"
}
}
}
# Store tldraw file in Supabase storage
file_path = f"{school_data['urn']}/tldraw.json"
file_options = {
"content-type": "application/json",
"x-upsert": "true", # Update if exists
"metadata": {
"establishment_urn": school_data['urn'],
"establishment_name": school_data['establishment_name']
}
}
try:
# Create a fresh service role client for storage operations
self.logger.info("Creating fresh service role client for storage operations")
service_client = create_client(
os.getenv("SUPABASE_URL"),
os.getenv("SERVICE_ROLE_KEY")
)
self.logger.debug(f"Service client created with URL: {os.getenv('SUPABASE_URL')}")
service_client.headers = {
"apiKey": os.getenv("SERVICE_ROLE_KEY"),
"Authorization": f"Bearer {os.getenv('SERVICE_ROLE_KEY')}",
"Content-Type": "application/json"
}
service_client.storage._client.headers.update({
"apiKey": os.getenv("SERVICE_ROLE_KEY"),
"Authorization": f"Bearer {os.getenv('SERVICE_ROLE_KEY')}",
"Content-Type": "application/json"
})
self.logger.debug("Headers set for service client and storage client")
# Upload to Supabase storage using service role client
self.logger.info(f"Uploading tldraw file for school {school_data['urn']}")
self.logger.debug(f"File path: {file_path}")
self.logger.debug(f"File options: {file_options}")
# First, ensure the bucket exists
self.logger.info("Checking if bucket cc.institutes exists")
try:
bucket = service_client.storage.get_bucket("cc.institutes")
self.logger.info("Bucket cc.institutes exists")
except Exception as bucket_error:
self.logger.error(f"Error checking bucket: {str(bucket_error)}")
if hasattr(bucket_error, 'response'):
self.logger.error(f"Bucket error response: {bucket_error.response.text if hasattr(bucket_error.response, 'text') else bucket_error.response}")
raise bucket_error
# Attempt the upload
self.logger.info("Attempting file upload")
result = service_client.storage.from_("cc.institutes").upload(
path=file_path,
file=json.dumps(tldraw_data).encode(),
file_options=file_options
)
self.logger.info(f"Upload successful. Result: {result}")
except Exception as upload_error:
self.logger.error(f"Error uploading tldraw file: {str(upload_error)}")
if hasattr(upload_error, 'response'):
self.logger.error(f"Upload error response: {upload_error.response.text if hasattr(upload_error.response, 'text') else upload_error.response}")
raise upload_error
# Create node in Neo4j using Neontology
with self.neontology as neo:
self.logger.info(f"Creating school node in Neo4j: {school_node.to_dict()}")
neo.create_or_merge_node(school_node, database="cc.institutes", operation="merge")
return {"status": "success", "node": school_node}
except Exception as e:
self.logger.error(f"Error creating school node: {str(e)}")
return {"status": "error", "message": str(e)}
def create_private_database(self, school_data):
"""Creates a private database for a specific school"""
try:
private_db_name = f"cc.institutes.{school_data['urn']}"
with self.driver.session() as session:
return self._extracted_from_create_private_database(
session, private_db_name, 'Created private database '
)
except Exception as e:
self.logger.error(f"Error creating private database: {str(e)}")
return {"status": "error", "message": str(e)}
# TODO Rename this here and in `create_schools_database` and `create_private_database`
def _extracted_from_create_private_database(self, session, arg1, arg2):
session_tools.create_database(session, arg1)
self.logger.info(f"{arg2}{arg1}")
return {
"status": "success",
"message": f"Database {arg1} created successfully",
}
def create_basic_structure(self, school_node, database_name):
"""Creates basic structural nodes in the specified database"""
try:
# Create filesystem paths
fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school")
# Create Department Structure node
department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}"
_, department_path = fs_handler.create_school_department_directory(school_node.path, "departments")
department_structure_node = entities.school_schemas.DepartmentNode(
unique_id=department_structure_node_unique_id,
path=department_path
)
# Create Curriculum Structure node
_, curriculum_path = fs_handler.create_school_curriculum_directory(school_node.path)
curriculum_node = school_structures.CurriculumStructureNode(
unique_id=f"CurriculumStructure_{school_node.unique_id}",
path=curriculum_path
)
# Create Pastoral Structure node
_, pastoral_path = fs_handler.create_school_pastoral_directory(school_node.path)
pastoral_node = school_structures.PastoralStructureNode(
unique_id=f"PastoralStructure_{school_node.unique_id}",
path=pastoral_path
)
with self.neontology as neo:
# Create nodes
neo.create_or_merge_node(department_structure_node, database=str(database_name), operation='merge')
fs_handler.create_default_tldraw_file(department_structure_node.path, department_structure_node.to_dict())
neo.create_or_merge_node(curriculum_node, database=str(database_name), operation='merge')
fs_handler.create_default_tldraw_file(curriculum_node.path, curriculum_node.to_dict())
neo.create_or_merge_node(pastoral_node, database=database_name, operation='merge')
fs_handler.create_default_tldraw_file(pastoral_node.path, pastoral_node.to_dict())
# Create relationships
neo.create_or_merge_relationship(
entity_relationships.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node),
database=database_name, operation='merge'
)
neo.create_or_merge_relationship(
entity_curriculum_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node),
database=database_name, operation='merge'
)
neo.create_or_merge_relationship(
entity_curriculum_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node),
database=database_name, operation='merge'
)
return {
"status": "success",
"message": "Basic structure created successfully",
"nodes": {
"department_structure": department_structure_node,
"curriculum_structure": curriculum_node,
"pastoral_structure": pastoral_node
}
}
except Exception as e:
self.logger.error(f"Error creating basic structure: {str(e)}")
return {"status": "error", "message": str(e)}
def create_detailed_structure(self, school_node, database_name, excel_file):
"""Creates detailed structural nodes from Excel file"""
try:
# First, store the Excel file in Supabase
file_path = f"{school_node.urn}/structure.xlsx"
file_options = {
"content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"x-upsert": "true"
}
# Upload Excel file to storage
self.supabase.storage.from_("cc.institutes").upload(
path=file_path,
file=excel_file,
file_options=file_options
)
# Process Excel file
dataframes = xl.create_dataframes(excel_file)
# Get existing basic structure nodes
with self.neontology as neo:
result = neo.cypher_read("""
MATCH (s:School {unique_id: $school_id})
OPTIONAL MATCH (s)-[:HAS_DEPARTMENT_STRUCTURE]->(ds:DepartmentStructure)
OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(cs:CurriculumStructure)
OPTIONAL MATCH (s)-[:HAS_PASTORAL_STRUCTURE]->(ps:PastoralStructure)
RETURN ds, cs, ps
""", {"school_id": school_node.unique_id}, database=database_name)
if not result:
raise Exception("Basic structure not found")
department_structure = result['ds']
curriculum_structure = result['cs']
pastoral_structure = result['ps']
# Create departments and subjects
unique_departments = dataframes['keystagesyllabuses']['Department'].dropna().unique()
fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school")
node_library = {}
with self.neontology as neo:
for department_name in unique_departments:
_, department_path = fs_handler.create_school_department_directory(school_node.path, department_name)
department_node = school_schemas.DepartmentNode(
unique_id=f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}",
department_name=department_name,
path=department_path
)
neo.create_or_merge_node(department_node, database=database_name, operation='merge')
fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict())
node_library[f'department_{department_name}'] = department_node
# Link to department structure
neo.create_or_merge_relationship(
entity_relationships.DepartmentStructureHasDepartment(
source=department_structure,
target=department_node
),
database=database_name,
operation='merge'
)
# Create year groups
year_groups = self.sort_year_groups(dataframes['yeargroupsyllabuses'])['YearGroup'].unique()
last_year_group_node = None
for year_group in year_groups:
numeric_year_group = pd.to_numeric(year_group, errors='coerce')
if pd.notna(numeric_year_group):
_, year_group_path = fs_handler.create_pastoral_year_group_directory(
pastoral_structure.path,
str(int(numeric_year_group))
)
year_group_node = pastoral_schemas.YearGroupNode(
unique_id=f"YearGroup_{school_node.unique_id}_YGrp{int(numeric_year_group)}",
year_group=str(int(numeric_year_group)),
year_group_name=f"Year {int(numeric_year_group)}",
path=year_group_path
)
neo.create_or_merge_node(year_group_node, database=database_name, operation='merge')
fs_handler.create_default_tldraw_file(year_group_node.path, year_group_node.to_dict())
node_library[f'year_group_{int(numeric_year_group)}'] = year_group_node
# Create sequential relationship
if last_year_group_node:
neo.create_or_merge_relationship(
curriculum_relationships.YearGroupFollowsYearGroup(
source=last_year_group_node,
target=year_group_node
),
database=database_name,
operation='merge'
)
last_year_group_node = year_group_node
# Link to pastoral structure
neo.create_or_merge_relationship(
curriculum_relationships.PastoralStructureIncludesYearGroup(
source=pastoral_structure,
target=year_group_node
),
database=database_name,
operation='merge'
)
# Create key stages
key_stages = dataframes['keystagesyllabuses']['KeyStage'].unique()
last_key_stage_node = None
for key_stage in sorted(key_stages):
_, key_stage_path = fs_handler.create_curriculum_key_stage_directory(
curriculum_structure.path,
str(key_stage)
)
key_stage_node = curriculum_schemas.KeyStageNode(
unique_id=f"KeyStage_{curriculum_structure.unique_id}_KStg{key_stage}",
key_stage_name=f"Key Stage {key_stage}",
key_stage=str(key_stage),
path=key_stage_path
)
neo.create_or_merge_node(key_stage_node, database=database_name, operation='merge')
fs_handler.create_default_tldraw_file(key_stage_node.path, key_stage_node.to_dict())
node_library[f'key_stage_{key_stage}'] = key_stage_node
# Create sequential relationship
if last_key_stage_node:
neo.create_or_merge_relationship(
curriculum_relationships.KeyStageFollowsKeyStage(
source=last_key_stage_node,
target=key_stage_node
),
database=database_name,
operation='merge'
)
last_key_stage_node = key_stage_node
# Link to curriculum structure
neo.create_or_merge_relationship(
curriculum_relationships.CurriculumStructureIncludesKeyStage(
source=curriculum_structure,
target=key_stage_node
),
database=database_name,
operation='merge'
)
return {
"status": "success",
"message": "Detailed structure created successfully",
"node_library": node_library
}
except Exception as e:
self.logger.error(f"Error creating detailed structure: {str(e)}")
return {"status": "error", "message": str(e)}
def sort_year_groups(self, df):
df = df.copy()
df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce')
return df.sort_values(by='YearGroupNumeric')
@@ -0,0 +1,54 @@
import os
from modules.logger_tool import initialise_logger
import modules.database.tools.neo4j_driver_tools as driver_tools
import modules.database.tools.neo4j_session_tools as session_tools
import modules.database.tools.neontology_tools as neon
import modules.database.schemas.entities as neo_entity
import modules.database.schemas.nodes.schools.curriculum as curriculum_schemas
import modules.database.schemas.relationships.curriculum_relationships as curriculum_relationships
import modules.database.schemas.relationships.entity_relationships as ent_rels
import modules.database.schemas.relationships.entity_curriculum_rels as ent_cur_rels
import pandas as pd
class SchoolSyllabusProvider:
def __init__(self):
self.driver = driver_tools.get_driver()
self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
def process_syllabus_data(self, school_node, database_name, dataframes):
"""Process syllabus data from Excel file and create nodes in the database"""
try:
# This method will contain the syllabus-specific processing code from the
# original SchoolCurriculumProvider, starting from where the comment
# "# Curriculum specific database initialisation begins here" was placed
# We'll implement this in the next iteration after confirming the basic
# structure changes work correctly
return {
"status": "success",
"message": "Syllabus data processed successfully"
}
except Exception as e:
self.logger.error(f"Error processing syllabus data: {str(e)}")
return {"status": "error", "message": str(e)}
def check_syllabus_status(self, school_node, database_name):
"""Check if syllabus data exists in the database"""
try:
with self.driver.session(database=database_name) as session:
result = session.run("""
MATCH (s:School {unique_id: $school_id})
OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(:CurriculumStructure)-[:INCLUDES_KEY_STAGE]->(:KeyStage)-[:INCLUDES_KEY_STAGE_SYLLABUS]->(ks:KeyStageSyllabus)
OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(:CurriculumStructure)-[:INCLUDES_KEY_STAGE]->(:KeyStage)-[:INCLUDES_YEAR_GROUP_SYLLABUS]->(ys:YearGroupSyllabus)
RETURN count(ks) > 0 OR count(ys) > 0 as has_syllabus
""", school_id=school_node.unique_id)
has_syllabus = result.single()["has_syllabus"]
return {"has_syllabus": has_syllabus}
except Exception as e:
self.logger.error(f"Error checking syllabus status: {str(e)}")
raise
@@ -0,0 +1,526 @@
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.init.init_calendar as init_calendar
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.tools.neontology_tools as neon
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
from datetime import timedelta, datetime
import pandas as pd
def create_school_timetable_from_dataframes(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("Initialising neo4j connection...")
neon.init_neontology_connection()
# Initialize the filesystem handler
fs_handler = ClassroomCopilotFilesystem(db_name, init_run_type="school")
school_df = dataframes['school']
if school_node is None:
logger.info("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': []
}
if school_node:
# Create the root timetable directory
_, timetable_path = fs_handler.create_school_timetable_directory(school_node.path)
else:
# Create the root timetable directory
_, timetable_path = fs_handler.create_school_timetable_directory()
# Create AcademicTimetable Node
school_timetable_unique_id = f"SchoolTimetable_{school_unique_id}_{school_year_start_date.year}_{school_year_end_date.year}"
school_timetable_node = timetable.SchoolTimetableNode(
unique_id=school_timetable_unique_id,
start_date=school_year_start_date,
end_date=school_year_end_date,
path=timetable_path
)
neon.create_or_merge_neontology_node(school_timetable_node, database=db_name, operation='merge')
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(school_timetable_node.path, school_timetable_node.to_dict())
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, entity_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, entity_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):
_, timetable_year_path = fs_handler.create_school_timetable_year_directory(timetable_path, year)
year_str = str(year)
academic_year_unique_id = f"AcademicYear_{school_timetable_unique_id}_{year}"
academic_year_node = timetable.AcademicYearNode(
unique_id=academic_year_unique_id,
year=year_str,
path=timetable_year_path
)
neon.create_or_merge_neontology_node(academic_year_node, database=db_name, operation='merge')
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(academic_year_node.path, academic_year_node.to_dict())
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':
_, timetable_term_path = fs_handler.create_school_timetable_academic_term_directory(
timetable_path=timetable_path,
term_name=term_name,
term_number=academic_term_number
)
term_node_unique_id = f"AcademicTerm_{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'),
path=timetable_term_path
)
academic_term_number += 1
else:
term_break_node_unique_id = f"AcademicTermBreak_{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')
)
neon.create_or_merge_neontology_node(term_node, database=db_name, operation='merge')
if isinstance(term_node, timetable.AcademicTermNode):
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(term_node.path, term_node.to_dict())
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')
if week_row['WeekType'] == 'Holiday':
week_node_unique_id = f"{week_row['WeekType']}Week_{school_timetable_unique_id}_Week_{week_row['WeekNumber']}"
week_node = week_node_class(
unique_id=week_node_unique_id,
start_date=datetime.strptime(week_start_date, '%Y-%m-%d')
)
else:
_, timetable_week_path = fs_handler.create_school_timetable_academic_week_directory(
timetable_path=timetable_path,
week_number=academic_week_number
)
week_node_unique_id = f"AcademicWeek_{school_timetable_unique_id}_Week_{week_row['WeekNumber']}"
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,
path=timetable_week_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}")
if isinstance(week_node, timetable.AcademicWeekNode):
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(week_node.path, week_node.to_dict())
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"{day_node_class.__name__}Day_{school_timetable_unique_id}_{day_number}",
'date': datetime.strptime(date_str, '%Y-%m-%d'),
'day_of_week': datetime.strptime(date_str, '%Y-%m-%d').strftime('%A')
}
if day_row['DayType'] == 'Academic':
day_node_data['academic_day'] = str(academic_day_number)
day_node_data['day_type'] = day_row['WeekType']
_, timetable_day_path = fs_handler.create_school_timetable_academic_day_directory(
timetable_path=timetable_path,
academic_day=academic_day_number
)
day_node_data['path'] = timetable_day_path
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):
fs_handler.create_default_tldraw_file(day_node.path, day_node.to_dict())
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"{period_node_class.__name__}_{school_timetable_unique_id}_Day_{academic_day_number}_Period_{period_of_day}"
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'])
}
logger.debug(f"Period node data: {period_node_data}")
if period_row['PeriodType'] in ['Academic', 'Registration']:
_, timetable_period_path = fs_handler.create_school_timetable_period_directory(
timetable_path=timetable_path,
academic_day=academic_day_number,
period_dir=f"{academic_or_registration_period_of_day}_{period_row['PeriodName'].replace(' ', '_')}"
)
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['path'] = timetable_period_path
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')
if isinstance(period_node, timetable.AcademicPeriodNode) or isinstance(period_node, timetable.RegistrationPeriodNode):
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(period_node.path, period_node.to_dict())
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
}
View File
+260
View File
@@ -0,0 +1,260 @@
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.schemas.nodes.calendars as calendar_schemas
import modules.database.schemas.relationships.calendars as calendar_relationships
import modules.database.schemas.relationships.calendar_sequence as calendar_sequence_relationships
import modules.database.schemas.relationships.owner_relationships as owner_relationships
import modules.database.tools.neontology_tools as neon
from datetime import timedelta, datetime
def create_calendar(db_name, start_date, end_date, attach_to_calendar_node=False, owner_node=None, time_chunk_node_length: int = None):
logger.info(f"Creating calendar for {start_date} to {end_date}")
logger.info(f"Initializing Neontology connection")
neon.init_neontology_connection()
created_years = {}
created_months = {}
created_weeks = {}
created_days = {}
last_year_node = None
last_month_node = None
last_week_node = None
last_day_node = None
calendar_nodes = {
'calendar_node': None,
'calendar_year_nodes': [],
'calendar_month_nodes': [],
'calendar_week_nodes': [],
'calendar_day_nodes': []
}
if attach_to_calendar_node and owner_node:
logger.info(f"Attaching calendar to owner's node {owner_node.unique_id} in database: {db_name}")
owner_unique_id = owner_node.unique_id
calendar_unique_id = f"{start_date.strftime('%Y-%m-%d')}_{end_date.strftime('%Y-%m-%d')}"
calendar_node = calendar_schemas.CalendarNode(
unique_id=calendar_unique_id,
name=f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}",
start_date=start_date,
end_date=end_date,
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(calendar_node, database=db_name, operation='merge')
calendar_nodes['calendar_node'] = calendar_node
logger.info(f"Calendar node created: {calendar_node.unique_id}")
import modules.database.schemas.relationships.owner_relationships as owner_relationships
neon.create_or_merge_neontology_relationship(
owner_relationships.OwnerHasCalendar(source=owner_node, target=calendar_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {owner_node.unique_id} to {calendar_node.unique_id}")
elif attach_to_calendar_node and not owner_node:
logger.info(f"Creating calendar for {start_date} to {end_date} in database: {db_name}")
calendar_node = calendar_schemas.CalendarNode(
unique_id=f"{start_date.strftime('%Y-%m-%d')}_{end_date.strftime('%Y-%m-%d')}",
name=f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}",
start_date=start_date,
end_date=end_date,
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(calendar_node, database=db_name, operation='merge')
calendar_nodes['calendar_node'] = calendar_node
logger.info(f"Calendar node created: {calendar_node.unique_id}")
else:
logger.error("Invalid combination of parameters for calendar creation.")
raise ValueError("Invalid combination of parameters for calendar creation.")
current_date = start_date
while current_date <= end_date:
year = current_date.year
month = current_date.month
day = current_date.day
iso_year, iso_week, iso_weekday = current_date.isocalendar()
calendar_year_unique_id = f"{year}"
if year not in created_years:
year_node = calendar_schemas.CalendarYearNode(
unique_id=calendar_year_unique_id,
year=str(year),
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(year_node, database=db_name, operation='merge')
calendar_nodes['calendar_year_nodes'].append(year_node)
created_years[year] = year_node
logger.info(f"Year node created: {year_node.unique_id}")
if attach_to_calendar_node:
neon.create_or_merge_neontology_relationship(
calendar_relationships.CalendarIncludesYear(source=calendar_node, target=year_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {calendar_node.unique_id} to {year_node.unique_id}")
if last_year_node:
neon.create_or_merge_neontology_relationship(
calendar_sequence_relationships.YearFollowsYear(source=last_year_node, target=year_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_year_node.unique_id} to {year_node.unique_id}")
last_year_node = year_node
calendar_month_unique_id = f"{year}_{month}"
month_key = f"{year}-{month}"
if month_key not in created_months:
month_node = calendar_schemas.CalendarMonthNode(
unique_id=calendar_month_unique_id,
year=str(year),
month=str(month),
month_name=datetime(year, month, 1).strftime('%B'),
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(month_node, database=db_name, operation='merge')
calendar_nodes['calendar_month_nodes'].append(month_node)
created_months[month_key] = month_node
logger.info(f"Month node created: {month_node.unique_id}")
# Check for the end of year transition for months
if last_month_node:
if int(month) == 1 and int(last_month_node.month) == 12 and int(last_month_node.year) == year - 1:
neon.create_or_merge_neontology_relationship(
calendar_sequence_relationships.MonthFollowsMonth(source=last_month_node, target=month_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}")
elif int(month) == int(last_month_node.month) + 1:
neon.create_or_merge_neontology_relationship(
calendar_sequence_relationships.MonthFollowsMonth(source=last_month_node, target=month_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}")
last_month_node = month_node
neon.create_or_merge_neontology_relationship(
calendar_relationships.YearIncludesMonth(source=year_node, target=month_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {year_node.unique_id} to {month_node.unique_id}")
calendar_week_unique_id = f"{iso_year}_{iso_week}"
week_key = f"{iso_year}-W{iso_week}"
if week_key not in created_weeks:
# Get the date of the first monday of the week
week_start_date = current_date - timedelta(days=current_date.weekday())
week_node = calendar_schemas.CalendarWeekNode(
unique_id=calendar_week_unique_id,
start_date=week_start_date,
week_number=str(iso_week),
iso_week=f"{iso_year}-W{iso_week:02}",
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge')
calendar_nodes['calendar_week_nodes'].append(week_node)
created_weeks[week_key] = week_node
logger.info(f"Week node created: {week_node.unique_id}")
if last_week_node and ((last_week_node.iso_week.split('-')[0] == str(iso_year) and int(last_week_node.week_number) == int(iso_week) - 1) or
(last_week_node.iso_week.split('-')[0] != str(iso_year) and int(last_week_node.week_number) == 52 and int(iso_week) == 1)):
neon.create_or_merge_neontology_relationship(
calendar_sequence_relationships.WeekFollowsWeek(source=last_week_node, target=week_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_week_node.unique_id} to {week_node.unique_id}")
last_week_node = week_node
neon.create_or_merge_neontology_relationship(
calendar_relationships.YearIncludesWeek(source=year_node, target=week_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {year_node.unique_id} to {week_node.unique_id}")
# Day node management
calendar_day_unique_id = f"{year}_{month}_{day}"
day_key = f"{year}-{month}-{day}"
day_node = calendar_schemas.CalendarDayNode(
unique_id=calendar_day_unique_id,
date=current_date,
day_of_week=current_date.strftime('%A'),
iso_day=f"{year}-{month:02}-{day:02}",
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge')
calendar_nodes['calendar_day_nodes'].append(day_node)
created_days[day_key] = day_node
logger.info(f"Day node created: {day_node.unique_id}")
if last_day_node:
neon.create_or_merge_neontology_relationship(
calendar_sequence_relationships.DayFollowsDay(source=last_day_node, target=day_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {last_day_node.unique_id} to {day_node.unique_id}")
last_day_node = day_node
neon.create_or_merge_neontology_relationship(
calendar_relationships.MonthIncludesDay(source=month_node, target=day_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {month_node.unique_id} to {day_node.unique_id}")
neon.create_or_merge_neontology_relationship(
calendar_relationships.WeekIncludesDay(source=week_node, target=day_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {week_node.unique_id} to {day_node.unique_id}")
current_date += timedelta(days=1)
if time_chunk_node_length:
time_chunk_interval = time_chunk_node_length
# Get every calendar day node and create time chunks of length time_chunk_node minutes for the whole day
for day_node in calendar_nodes['calendar_day_nodes']:
total_time_chunks_in_day = (24 * 60) / time_chunk_interval
for i in range(total_time_chunks_in_day):
time_chunk_unique_id = f"{day_node.unique_id}_{i}"
time_chunk_start_time = day_node.date.time() + timedelta(minutes=i * time_chunk_interval)
time_chunk_end_time = time_chunk_start_time + timedelta(minutes=time_chunk_interval)
time_chunk_node = calendar_schemas.CalendarTimeChunkNode(
unique_id=time_chunk_unique_id,
start_time=time_chunk_start_time,
end_time=time_chunk_end_time,
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(time_chunk_node, database=db_name, operation='merge')
calendar_nodes['calendar_time_chunk_nodes'].append(time_chunk_node)
logger.info(f"Time chunk node created: {time_chunk_node.unique_id}")
# Create a relationship between the time chunk node and the day node
neon.create_or_merge_neontology_relationship(
calendar_relationships.DayIncludesTimeChunk(source=day_node, target=time_chunk_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {day_node.unique_id} to {time_chunk_node.unique_id}")
# Create sequential relationship between the time chunk nodes
if i > 0:
neon.create_or_merge_neontology_relationship(
calendar_sequence_relationships.TimeChunkFollowsTimeChunk(source=calendar_nodes['calendar_time_chunk_nodes'][i-1], target=time_chunk_node),
database=db_name,
operation='merge'
)
logger.info(f"Relationship created from {calendar_nodes['calendar_time_chunk_nodes'][i-1].unique_id} to {time_chunk_node.unique_id}")
logger.info(f'Created calendar: {calendar_nodes["calendar_node"].unique_id}')
return calendar_nodes
+158
View File
@@ -0,0 +1,158 @@
import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
from modules.database.schemas.nodes.schools.schools import SchoolNode
from modules.database.supabase.utils.client import SupabaseServiceRoleClient, CreateBucketOptions
import modules.database.init.init_school_timetable as init_school_timetable
import modules.database.tools.neontology_tools as neon
def create_school_buckets(school_id: str, school_type: str, school_name: str, admin_access_token: str) -> dict:
"""Create storage buckets for a school
Args:
school_id: The unique identifier for the school
school_type: The type of school (e.g., 'development')
school_name: The display name of the school
admin_access_token: The admin access token for Supabase operations
Returns:
Dictionary containing results of bucket creation operations
"""
logger.info(f"Creating storage buckets for school {school_name} ({school_type}/{school_id})")
storage_client = SupabaseServiceRoleClient.for_admin(admin_access_token)
base_path = f"cc.institutes.{school_type}.{school_id}"
buckets = [
# Main school buckets
{
"id": f"{base_path}.public",
"options": CreateBucketOptions(
name=f"{school_type.title()} School Files - {school_name} - Public Files",
public=True,
file_size_limit=50 * 1024 * 1024,
allowed_mime_types=[
'image/*', 'video/*', 'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.*',
'text/plain', 'text/csv', 'application/json'
]
)
},
{
"id": f"{base_path}.private",
"options": CreateBucketOptions(
name=f"{school_type.title()} School Files - {school_name} - Private Files",
public=False,
file_size_limit=50 * 1024 * 1024,
allowed_mime_types=[
'image/*', 'video/*', 'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.*',
'text/plain', 'text/csv', 'application/json'
]
)
},
# Curriculum buckets
{
"id": f"{base_path}.curriculum.public",
"options": CreateBucketOptions(
name=f"{school_type.title()} School Files - {school_name} - Curriculum Public Files",
public=True,
file_size_limit=50 * 1024 * 1024,
allowed_mime_types=[
'image/*', 'video/*', 'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.*',
'text/plain', 'text/csv', 'application/json'
]
)
},
{
"id": f"{base_path}.curriculum.private",
"options": CreateBucketOptions(
name=f"{school_type.title()} School Files - {school_name} - Curriculum Private Files",
public=False,
file_size_limit=50 * 1024 * 1024,
allowed_mime_types=[
'image/*', 'video/*', 'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.*',
'text/plain', 'text/csv', 'application/json'
]
)
}
]
results = {}
for bucket in buckets:
try:
result = storage_client.create_bucket(bucket["id"], bucket["options"])
results[bucket["id"]] = {
"status": "success",
"result": result
}
logger.info(f"Successfully created bucket {bucket['id']}")
except Exception as e:
logger.error(f"Error creating school bucket {bucket['id']}: {str(e)}")
results[bucket["id"]] = {
"status": "error",
"error": str(e)
}
return results
def create_school(db_name: str, id: str, name: str, website: str, school_type: str, is_public: bool = True, school_node: SchoolNode | None = None, dataframes=None):
if not name or not id or not website or not school_type:
logger.error("School name, id, website and school_type are required to create a school.")
raise ValueError("School name, id, website and school_type are required to create a school.")
logger.info(f"Initialising neo4j connection...")
neon.init_neontology_connection()
# Create School Node if not provided
if not school_node:
if is_public:
school_node = SchoolNode(
unique_id=f'School_{id}',
tldraw_snapshot="",
id=id,
name=name,
website=website,
school_type=school_type
)
else:
# Create private school node with default values
school_node = SchoolNode(
unique_id=f'School_{id}',
tldraw_snapshot="",
id=id,
name=name,
website=website,
school_type=school_type,
establishment_number="0000",
establishment_name=name,
establishment_type="Default",
establishment_status="Open",
phase_of_education="All",
statutory_low_age=11,
statutory_high_age=18,
school_capacity=1000
)
# First create/merge the school node in the main cc.institutes database
logger.info(f"Creating school node in main cc.institutes database...")
neon.create_or_merge_neontology_node(school_node, database="cc.institutes", operation='merge')
# Then create/merge the school node in the specific school database
logger.info(f"Creating school node in specific database {db_name}...")
neon.create_or_merge_neontology_node(school_node, database=db_name, operation='merge')
school_nodes = {
'school_node': school_node,
'db_name': db_name
}
if dataframes is not None:
logger.info(f"Creating school timetable for {name} with {len(dataframes)} dataframes...")
school_timetable_nodes = init_school_timetable.create_school_timetable(dataframes, db_name, school_node)
school_nodes['school_timetable_nodes'] = school_timetable_nodes
else:
logger.warning(f"No dataframes provided for {name}, skipping school timetable...")
logger.info(f"School {name} created successfully...")
return school_nodes
@@ -0,0 +1,697 @@
import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
import pandas as pd
import modules.database.tools.neontology_tools as neon
import modules.database.schemas.nodes.schools.schools as school_nodes
import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes
import modules.database.schemas.nodes.schools.pastoral as pastoral_nodes
import modules.database.schemas.nodes.structures.schools as school_structures
import modules.database.schemas.relationships.curriculum_relationships as curriculum_relationships
import modules.database.schemas.relationships.entity_relationships as ent_rels
import modules.database.schemas.relationships.entity_curriculum_rels as ent_cur_rels
# Default values for nodes
default_topic_values = {
'topic_assessment_type': 'Null',
'topic_type': 'Null',
'total_number_of_lessons_for_topic': '1',
'topic_title': 'Null'
}
default_topic_lesson_values = {
'topic_lesson_title': 'Null',
'topic_lesson_type': 'Null',
'topic_lesson_length': '1',
'topic_lesson_suggested_activities': 'Null',
'topic_lesson_skills_learned': 'Null',
'topic_lesson_weblinks': 'Null',
}
default_learning_statement_values = {
'lesson_learning_statement': 'Null',
'lesson_learning_statement_type': 'Student learning outcome'
}
# Helper function to sort year groups numerically where possible
def sort_year_groups(df):
df = df.copy()
df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce')
return df.sort_values(by='YearGroupNumeric')
def create_curriculum(dataframes, db_name: str, curriculum_db_name: str, school_node: school_nodes.SchoolNode):
logger.info(f"Initialising neo4j connection...")
neon.init_neontology_connection()
keystagesyllabus_df = dataframes['keystagesyllabuses']
yeargroupsyllabus_df = dataframes['yeargroupsyllabuses']
topic_df = dataframes['topics']
lesson_df = dataframes['lessons']
statement_df = dataframes['statements']
# resource_df = dataframes['resources'] # TODO
node_library = {}
node_library['key_stage_nodes'] = {}
node_library['year_group_nodes'] = {}
node_library['key_stage_syllabus_nodes'] = {}
node_library['year_group_syllabus_nodes'] = {}
node_library['topic_nodes'] = {}
node_library['topic_lesson_nodes'] = {}
node_library['statement_nodes'] = {}
node_library['department_nodes'] = {}
node_library['subject_nodes'] = {}
curriculum_node = None
pastoral_node = None
key_stage_nodes_created = {}
year_group_nodes_created = {}
last_year_group_node = None
last_key_stage_node = None
# Create Department Structure node
department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}"
department_structure_node = school_structures.DepartmentStructureNode(
unique_id=department_structure_node_unique_id,
tldraw_snapshot=""
)
# Create in school database only
neon.create_or_merge_neontology_node(department_structure_node, database=db_name, operation='merge')
node_library['department_structure_node'] = department_structure_node
# Link Department Structure to School
neon.create_or_merge_neontology_relationship(
ent_rels.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node),
database=db_name, operation='merge'
)
logger.info(f"Created department structure node and linked to school")
curriculum_structure_node_unique_id = f"CurriculumStructure_{school_node.unique_id}"
curriculum_node = school_structures.CurriculumStructureNode(
unique_id=curriculum_structure_node_unique_id,
tldraw_snapshot=""
)
# Create in school database only
neon.create_or_merge_neontology_node(curriculum_node, database=db_name, operation='merge')
node_library['curriculum_node'] = curriculum_node
# Create relationship in school database only
neon.create_or_merge_neontology_relationship(
ent_cur_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node),
database=db_name, operation='merge'
)
logger.info(f"Created curriculum node and relationship with school")
pastoral_structure_node_unique_id = f"PastoralStructure_{school_node.unique_id}"
pastoral_node = school_structures.PastoralStructureNode(
unique_id=pastoral_structure_node_unique_id,
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(pastoral_node, database=db_name, operation='merge')
node_library['pastoral_node'] = pastoral_node
neon.create_or_merge_neontology_relationship(
ent_cur_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node),
database=db_name, operation='merge'
)
logger.info(f"Created pastoral node and relationship with school")
# Create departments and subjects
# First get unique departments
unique_departments = keystagesyllabus_df['Department'].dropna().unique()
for department_name in unique_departments:
department_unique_id = f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}"
department_node = school_nodes.DepartmentNode(
unique_id=department_unique_id,
name=department_name,
tldraw_snapshot=""
)
# Create department in school database only
neon.create_or_merge_neontology_node(department_node, database=db_name, operation='merge')
node_library['department_nodes'][department_name] = department_node
# Link department to department structure in school database
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node),
database=db_name, operation='merge'
)
logger.info(f"Created department node for {department_name} and linked to department structure")
# Create subjects and link to departments
# First get unique subjects from key stage syllabuses (which have department info)
unique_subjects = keystagesyllabus_df[['Subject', 'SubjectCode', 'Department']].drop_duplicates()
# Then add any additional subjects from year group syllabuses (without department info)
additional_subjects = yeargroupsyllabus_df[['Subject', 'SubjectCode']].drop_duplicates()
additional_subjects = additional_subjects[~additional_subjects['SubjectCode'].isin(unique_subjects['SubjectCode'])]
# Process subjects from key stage syllabuses first (these have department info)
for _, subject_row in unique_subjects.iterrows():
subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}"
department_node = node_library['department_nodes'].get(subject_row['Department'])
if not department_node:
logger.warning(f"No department found for subject {subject_row['Subject']} with code {subject_row['SubjectCode']}")
continue
subject_node = curriculum_nodes.SubjectNode(
unique_id=subject_unique_id,
id=subject_row['SubjectCode'],
name=subject_row['Subject'],
tldraw_snapshot=""
)
# Create subject in both databases
neon.create_or_merge_neontology_node(subject_node, database=db_name, operation='merge')
neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge')
node_library['subject_nodes'][subject_row['Subject']] = subject_node
# Link subject to department in school database only
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentManagesSubject(source=department_node, target=subject_node),
database=db_name, operation='merge'
)
logger.info(f"Created subject node for {subject_row['Subject']} and linked to department {subject_row['Department']}")
# Process any additional subjects from year group syllabuses (these won't have department info)
for _, subject_row in additional_subjects.iterrows():
subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}"
# Create in a special "Unassigned" department
unassigned_dept_name = "Unassigned Department"
if unassigned_dept_name not in node_library['department_nodes']:
department_node = school_nodes.DepartmentNode(
unique_id=f"Department_{school_node.unique_id}_Unassigned",
name=unassigned_dept_name,
tldraw_snapshot=""
)
neon.create_or_merge_neontology_node(department_node, database=db_name, operation='merge')
node_library['department_nodes'][unassigned_dept_name] = department_node
# Link unassigned department to department structure
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node),
database=db_name, operation='merge'
)
logger.info(f"Created unassigned department node and linked to department structure")
subject_node = curriculum_nodes.SubjectNode(
unique_id=subject_unique_id,
id=subject_row['SubjectCode'],
name=subject_row['Subject'],
tldraw_snapshot=""
)
# Create subject in both databases
neon.create_or_merge_neontology_node(subject_node, database=db_name, operation='merge')
neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge')
node_library['subject_nodes'][subject_row['Subject']] = subject_node
# Link subject to unassigned department in school database only
neon.create_or_merge_neontology_relationship(
ent_rels.DepartmentManagesSubject(
source=node_library['department_nodes'][unassigned_dept_name],
target=subject_node
),
database=db_name, operation='merge'
)
logger.warning(f"Created subject node for {subject_row['Subject']} in unassigned department")
# Process key stages and syllabuses
logger.info(f"Processing key stages")
last_key_stage_node = None
# Track last syllabus nodes per subject
last_key_stage_syllabus_nodes = {} # Dictionary to track last key stage syllabus node per subject
last_year_group_syllabus_nodes = {} # Dictionary to track last year group syllabus node per subject
topics_processed = set() # Track which topics have been processed
lessons_processed = set() # Track which lessons have been processed
statements_processed = set() # Track which statements have been processed
# First create all key stage nodes and key stage syllabus nodes
for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows():
key_stage = str(ks_row['KeyStage'])
logger.debug(f"Processing key stage syllabus row - Subject: {ks_row['Subject']}, Key Stage: {key_stage}")
subject_node = node_library['subject_nodes'].get(ks_row['Subject'])
if not subject_node:
logger.warning(f"No subject node found for subject {ks_row['Subject']}")
continue
if key_stage not in key_stage_nodes_created:
key_stage_node_unique_id = f"KeyStage_{curriculum_node.unique_id}_KStg{key_stage}"
key_stage_node = curriculum_nodes.KeyStageNode(
unique_id=key_stage_node_unique_id,
name=f"Key Stage {key_stage}",
key_stage=str(key_stage),
tldraw_snapshot=""
)
# Create key stage node in both databases
neon.create_or_merge_neontology_node(key_stage_node, database=db_name, operation='merge')
neon.create_or_merge_neontology_node(key_stage_node, database=curriculum_db_name, operation='merge')
key_stage_nodes_created[key_stage] = key_stage_node
node_library['key_stage_nodes'][key_stage] = key_stage_node
# Create relationship with curriculum structure in school database only
neon.create_or_merge_neontology_relationship(
curriculum_relationships.CurriculumStructureIncludesKeyStage(source=curriculum_node, target=key_stage_node),
database=db_name, operation='merge'
)
logger.info(f"Created key stage node {key_stage_node_unique_id} and relationship with curriculum structure")
# Create sequential relationship between key stages in both databases
if last_key_stage_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between key stages {last_key_stage_node.unique_id} and {key_stage_node.unique_id}")
last_key_stage_node = key_stage_node
# Create key stage syllabus under the subject's curriculum directory
key_stage_syllabus_node_unique_id = f"KeyStageSyllabus_{curriculum_node.unique_id}_{ks_row['Title'].replace(' ', '')}"
logger.debug(f"Creating key stage syllabus node for {ks_row['Subject']} KS{key_stage} with ID {ks_row['ID']}")
key_stage_syllabus_node_unique_id = f"KeyStageSyllabus_{curriculum_node.unique_id}_{ks_row['Title'].replace(' ', '')}"
key_stage_syllabus_node = curriculum_nodes.KeyStageSyllabusNode(
unique_id=key_stage_syllabus_node_unique_id,
id=ks_row['ID'],
name=ks_row['Title'],
key_stage=str(ks_row['KeyStage']),
subject_name=ks_row['Subject'],
tldraw_snapshot=""
)
# Create key stage syllabus node in both databases
neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=db_name, operation='merge')
neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=curriculum_db_name, operation='merge')
node_library['key_stage_syllabus_nodes'][ks_row['ID']] = key_stage_syllabus_node
logger.debug(f"Created key stage syllabus node {key_stage_syllabus_node_unique_id} for {ks_row['Subject']} KS{key_stage}")
# Link key stage syllabus to its subject in both databases
if subject_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between subject {subject_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}")
# Link key stage syllabus to its key stage in both databases
key_stage_node = key_stage_nodes_created.get(key_stage)
if key_stage_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between key stage {key_stage_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}")
# Create sequential relationship between key stage syllabuses in both databases
last_key_stage_syllabus_node = last_key_stage_syllabus_nodes.get(ks_row['Subject'])
if last_key_stage_syllabus_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between key stage syllabuses {last_key_stage_syllabus_node.unique_id} and {key_stage_syllabus_node.unique_id}")
last_key_stage_syllabus_nodes[ks_row['Subject']] = key_stage_syllabus_node
# Now process year groups and their syllabuses
for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows():
key_stage = str(ks_row['KeyStage'])
related_yeargroups = sort_year_groups(yeargroupsyllabus_df[yeargroupsyllabus_df['KeyStage'] == ks_row['KeyStage']])
logger.info(f"Processing year groups for key stage {key_stage}")
for yg_index, yg_row in related_yeargroups.iterrows():
year_group = yg_row['YearGroup']
subject_code = yg_row['SubjectCode']
numeric_year_group = pd.to_numeric(year_group, errors='coerce')
if pd.notna(numeric_year_group):
numeric_year_group = int(numeric_year_group)
if numeric_year_group not in year_group_nodes_created:
year_group_node_unique_id = f"YearGroup_{school_node.unique_id}_YGrp{numeric_year_group}"
year_group_node = pastoral_nodes.YearGroupNode(
unique_id=year_group_node_unique_id,
year_group=str(numeric_year_group),
name=f"Year {numeric_year_group}, {year_group}",
tldraw_snapshot=""
)
# Create year group node in both databases but use same directory
neon.create_or_merge_neontology_node(year_group_node, database=db_name, operation='merge')
neon.create_or_merge_neontology_node(year_group_node, database=curriculum_db_name, operation='merge')
# Create sequential relationship between year groups in both databases
if last_year_group_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between year groups {last_year_group_node.unique_id} and {year_group_node.unique_id} across key stages")
last_year_group_node = year_group_node
# Create relationship with Pastoral Structure in school database only
neon.create_or_merge_neontology_relationship(
curriculum_relationships.PastoralStructureIncludesYearGroup(source=pastoral_node, target=year_group_node),
database=db_name, operation='merge'
)
logger.info(f"Created year group node {year_group_node_unique_id} and relationship with pastoral structure")
year_group_nodes_created[numeric_year_group] = year_group_node
node_library['year_group_nodes'][str(numeric_year_group)] = year_group_node
# Create year group syllabus nodes in both databases
year_group_node = year_group_nodes_created.get(numeric_year_group)
if year_group_node:
year_group_syllabus_node_unique_id = f"YearGroupSyllabus_{school_node.unique_id}_{yg_row['ID']}"
year_group_syllabus_node = pastoral_nodes.YearGroupSyllabusNode(
unique_id=year_group_syllabus_node_unique_id,
id=yg_row['ID'],
name=yg_row['Title'],
year_group=str(yg_row['YearGroup']),
subject_name=yg_row['Subject'],
tldraw_snapshot=""
)
# Create year group syllabus node in both databases but use same directory
neon.create_or_merge_neontology_node(year_group_syllabus_node, database=db_name, operation='merge')
neon.create_or_merge_neontology_node(year_group_syllabus_node, database=curriculum_db_name, operation='merge')
node_library['year_group_syllabus_nodes'][yg_row['ID']] = year_group_syllabus_node
# Create sequential relationship between year group syllabuses in both databases
last_year_group_syllabus_node = last_year_group_syllabus_nodes.get(yg_row['Subject'])
# Only create sequential relationship if this year group is higher than the last one
if last_year_group_syllabus_node:
last_year = pd.to_numeric(last_year_group_syllabus_node.year_group, errors='coerce')
current_year = pd.to_numeric(year_group_syllabus_node.year_group, errors='coerce')
if pd.notna(last_year) and pd.notna(current_year) and current_year > last_year:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between year group syllabuses {last_year_group_syllabus_node.unique_id} and {year_group_syllabus_node.unique_id}")
last_year_group_syllabus_nodes[yg_row['Subject']] = year_group_syllabus_node
# Create relationships in both databases using MATCH to avoid cartesian products
subject_node = node_library['subject_nodes'].get(yg_row['Subject'])
if subject_node:
# Link to subject
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between subject {subject_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Link to year group
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between year group {year_group_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Link to key stage syllabus if it exists for the same subject
key_stage_syllabus_node = node_library['key_stage_syllabus_nodes'].get(ks_row['ID'])
if key_stage_syllabus_node and yg_row['Subject'] == ks_row['Subject']:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node),
database=db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between key stage syllabus {key_stage_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Process topics for this year group syllabus only if not already processed
topics_for_syllabus = topic_df[topic_df['SyllabusYearID'] == yg_row['ID']]
for _, topic_row in topics_for_syllabus.iterrows():
if topic_row['TopicID'] in topics_processed:
continue
topics_processed.add(topic_row['TopicID'])
# Get the correct subject from the topic row
topic_subject = topic_row['SyllabusSubject']
topic_key_stage = topic_row['SyllabusKeyStage']
logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage}")
logger.debug(f"Available key stage syllabus nodes: {[node.subject_name + '_KS' + node.key_stage for node in node_library['key_stage_syllabus_nodes'].values()]}")
# Find the key stage syllabus node by iterating through all nodes
matching_syllabus_node = None
for syllabus_node in node_library['key_stage_syllabus_nodes'].values():
logger.debug(f"Checking syllabus node - Subject: {syllabus_node.subject_name}, Key Stage: {syllabus_node.key_stage}")
logger.debug(f"Comparing with - Subject: {topic_subject}, Key Stage: {str(topic_key_stage)}")
logger.debug(f"Types - Node Subject: {type(syllabus_node.subject_name)}, Topic Subject: {type(topic_subject)}")
logger.debug(f"Types - Node Key Stage: {type(syllabus_node.key_stage)}, Topic Key Stage: {type(str(topic_key_stage))}")
if (syllabus_node.subject_name == topic_subject and
syllabus_node.key_stage == str(topic_key_stage)):
matching_syllabus_node = syllabus_node
logger.debug(f"Found matching syllabus node: {syllabus_node.unique_id}")
break
if not matching_syllabus_node:
logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation")
continue
topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}"
topic_node = curriculum_nodes.TopicNode(
unique_id=topic_node_unique_id,
id=topic_row['TopicID'],
name=topic_row.get('TopicTitle', default_topic_values['topic_title']),
total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])),
type=topic_row.get('TopicType', default_topic_values['topic_type']),
assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']),
tldraw_snapshot=""
)
# Create topic node in curriculum database only
neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge')
node_library['topic_nodes'][topic_row['TopicID']] = topic_node
# Link topic to key stage syllabus as well as year group syllabus
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node),
database=curriculum_db_name, operation='merge'
)
neon.create_or_merge_neontology_relationship(
curriculum_relationships.YearGroupSyllabusIncludesTopic(source=year_group_syllabus_node, target=topic_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationships between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}")
# Process lessons for this topic only if not already processed
lessons_for_topic = lesson_df[
(lesson_df['TopicID'] == topic_row['TopicID']) &
(lesson_df['SyllabusSubject'] == topic_subject)
].copy()
lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str)
lessons_for_topic = lessons_for_topic.sort_values('Lesson')
previous_lesson_node = None
for _, lesson_row in lessons_for_topic.iterrows():
if lesson_row['LessonID'] in lessons_processed:
continue
lessons_processed.add(lesson_row['LessonID'])
lesson_node = curriculum_nodes.TopicLessonNode(
unique_id=f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}",
id=lesson_row['LessonID'],
name=lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']),
type=lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']),
length=str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])),
suggested_activities=str(lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities'])),
skills_learned=str(lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned'])),
weblinks=str(lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks'])),
tldraw_snapshot=""
)
# Create lesson node in curriculum database only
neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge')
node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node
# Link lesson to topic
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}")
# Create sequential relationships between lessons
if lesson_row['Lesson'].isdigit() and previous_lesson_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}")
previous_lesson_node = lesson_node
# Process learning statements for this lesson only if not already processed
statements_for_lesson = statement_df[
(statement_df['LessonID'] == lesson_row['LessonID']) &
(statement_df['SyllabusSubject'] == topic_subject)
]
for _, statement_row in statements_for_lesson.iterrows():
if statement_row['StatementID'] in statements_processed:
continue
statements_processed.add(statement_row['StatementID'])
statement_node = curriculum_nodes.LearningStatementNode(
unique_id=f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}",
id=statement_row['StatementID'],
name=statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']),
type=statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']),
tldraw_snapshot=""
)
# Create statement node in curriculum database only
neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge')
node_library['statement_nodes'][statement_row['StatementID']] = statement_node
# Link learning statement to lesson
neon.create_or_merge_neontology_relationship(
curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}")
else:
logger.warning(f"No year group node found for year group {year_group}, skipping syllabus creation")
# After processing all year groups and their syllabuses, process any remaining topics
logger.info("Processing topics without year groups")
for _, topic_row in topic_df.iterrows():
if topic_row['TopicID'] in topics_processed:
continue
topic_subject = topic_row['SyllabusSubject']
topic_key_stage = topic_row['SyllabusKeyStage']
logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage} without year group")
# Find the key stage syllabus node
matching_syllabus_node = None
for syllabus_node in node_library['key_stage_syllabus_nodes'].values():
if (syllabus_node.subject_name == topic_subject and
syllabus_node.key_stage == str(topic_key_stage)):
matching_syllabus_node = syllabus_node
break
if not matching_syllabus_node:
logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation")
continue
topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}"
topic_node = curriculum_nodes.TopicNode(
unique_id=topic_node_unique_id,
id=topic_row['TopicID'],
name=topic_row.get('TopicTitle', default_topic_values['topic_title']),
total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])),
type=topic_row.get('TopicType', default_topic_values['topic_type']),
assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']),
tldraw_snapshot=""
)
# Create topic node in curriculum database only
neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge')
node_library['topic_nodes'][topic_row['TopicID']] = topic_node
topics_processed.add(topic_row['TopicID'])
# Link topic to key stage syllabus
neon.create_or_merge_neontology_relationship(
curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created relationship between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id}")
# Process lessons for this topic
lessons_for_topic = lesson_df[
(lesson_df['TopicID'] == topic_row['TopicID']) &
(lesson_df['SyllabusSubject'] == topic_subject)
].copy()
lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str)
lessons_for_topic = lessons_for_topic.sort_values('Lesson')
previous_lesson_node = None
for _, lesson_row in lessons_for_topic.iterrows():
if lesson_row['LessonID'] in lessons_processed:
continue
lessons_processed.add(lesson_row['LessonID'])
lesson_node = curriculum_nodes.TopicLessonNode(
unique_id=f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}",
id=lesson_row['LessonID'],
name=lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']),
type=lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']),
length=str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])),
suggested_activities=str(lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities'])),
skills_learned=str(lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned'])),
weblinks=str(lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks'])),
tldraw_snapshot=""
)
# Create lesson node in curriculum database only
neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge')
node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node
# Link lesson to topic
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}")
# Create sequential relationships between lessons
if lesson_row['Lesson'].isdigit() and previous_lesson_node:
neon.create_or_merge_neontology_relationship(
curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}")
previous_lesson_node = lesson_node
# Process learning statements for this lesson
statements_for_lesson = statement_df[
(statement_df['LessonID'] == lesson_row['LessonID']) &
(statement_df['SyllabusSubject'] == topic_subject)
]
for _, statement_row in statements_for_lesson.iterrows():
if statement_row['StatementID'] in statements_processed:
continue
statements_processed.add(statement_row['StatementID'])
statement_node = curriculum_nodes.LearningStatementNode(
unique_id=f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}",
id=statement_row['StatementID'],
name=statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']),
type=statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']),
tldraw_snapshot=""
)
# Create statement node in curriculum database only
neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge')
node_library['statement_nodes'][statement_row['StatementID']] = statement_node
# Link learning statement to lesson
neon.create_or_merge_neontology_relationship(
curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node),
database=curriculum_db_name, operation='merge'
)
logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}")
return node_library
@@ -0,0 +1,487 @@
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
}
+379
View File
@@ -0,0 +1,379 @@
import os
from datetime import timedelta, datetime
from abc import ABC, abstractmethod
from typing import Dict, Optional, Any, Union
from modules.database.services.neo4j_service import Neo4jService
import modules.database.schemas.nodes.users as user_nodes
import modules.database.schemas.nodes.workers.workers as worker_nodes
import modules.database.init.init_calendar as init_calendar
import modules.database.schemas.relationships.entity_relationships as entity_relationships
import modules.database.tools.neontology_tools as neon
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
def create_and_check_db(db_name):
neo4j_service = Neo4jService()
neo4j_service.create_database(db_name)
database_status = neo4j_service.check_database_exists(db_name)
if not database_status['exists']:
raise ValueError(f"Database {db_name} not found")
return database_status
class UserCreator(ABC):
def __init__(self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date):
cc_schools_db_name = "cc.institutes" # Fix the TODO
self.cc_users_db_name = cc_users_db_name
self.user_db_name = f"{cc_users_db_name}.{user_type}.{cc_username}"
self.worker_db_name = f"{cc_schools_db_name}.{user_type}.{cc_username}"
self.user_type = user_type
self.worker_type = worker_type
self.cc_username = cc_username
self.user_email = user_email
self.worker_email = worker_email
self.user_name = user_name
self.worker_name = worker_name
self.user_id = user_id
self.user_nodes: Dict[str, Optional[Any]] = {
'default_user_node': None,
'private_user_node': None,
'worker_node': None,
'calendar_node': None
}
if calendar_start_date and calendar_end_date:
self.calendar_start_date = calendar_start_date
self.calendar_end_date = calendar_end_date
else:
logger.warning("No calendar start and end date provided, using default values")
self.calendar_start_date = datetime.now().date()
self.calendar_end_date = (datetime.now() + timedelta(days=5)).date()
@abstractmethod
def create_user(self):
pass
def create_user_node(self, db_name: str):
logger.info(f"Module is creating {self.cc_users_db_name} user node for {self.user_type} user {self.cc_username}")
try:
user_node = self._create_user_node(db_name)
logger.debug(f"User node creation completed for {self.cc_users_db_name} user node for {self.user_type} user {self.cc_username}: {user_node.to_dict()}")
return user_node
except Exception as e:
logger.error(f"Error creating user node: {e}")
raise
def _create_user_node(self, db_name: str):
# Ensure Neontology is initialized
neon.init_neontology_connection()
user_node = user_nodes.UserNode(
unique_id=f"{self.user_id}",
tldraw_snapshot="",
cc_username=f"{self.cc_username}",
user_email=f"{self.user_email}",
user_name=f"{self.user_name}",
user_db_name=f"{self.user_db_name}",
user_type=f"{self.user_type}",
)
logger.debug(f"User node template created: {user_node.to_dict()}. Writing to database {db_name}")
neon.create_or_merge_neontology_node(node=user_node, database=db_name, operation='merge')
logger.info(f"User node created: {user_node.to_dict()}")
return user_node
def create_storage_bucket(self, bucket_id: str, bucket_name: str, access_token: Optional[str] = None) -> bool:
"""Create public and private storage buckets for the user using their access token or service role during initialization"""
logger.info(f"Creating storage buckets for user {self.cc_username}")
try:
from modules.database.supabase.utils.client import SupabaseServiceRoleClient, SupabaseAnonClient, CreateBucketOptions
# During initialization (no access token provided), use service role
if not access_token:
logger.info("Using service role client for bucket creation during initialization")
supabase = SupabaseServiceRoleClient()
else:
# For regular operations, use the user's access token
logger.info("Using user token for bucket creation")
supabase = SupabaseAnonClient.for_user(access_token)
# Create both public and private buckets
buckets = [
{
"id": f"{bucket_id}.public",
"options": CreateBucketOptions(
name=f"{bucket_name} - Public Files",
public=True,
file_size_limit=50 * 1024 * 1024, # 50MB
allowed_mime_types=[
'image/*', 'video/*', 'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.*',
'text/plain', 'text/csv', 'application/json'
]
)
},
{
"id": f"{bucket_id}.private",
"options": CreateBucketOptions(
name=f"{bucket_name} - Private Files",
public=False,
file_size_limit=50 * 1024 * 1024, # 50MB
allowed_mime_types=[
'image/*', 'video/*', 'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.*',
'text/plain', 'text/csv', 'application/json'
]
)
}
]
success = True
for bucket in buckets:
try:
result = supabase.create_bucket(bucket["id"], bucket["options"])
if not result:
logger.error(f"Failed to create bucket {bucket['id']}")
success = False
else:
logger.info(f"Successfully created bucket {bucket['id']}")
except Exception as e:
logger.error(f"Error creating bucket {bucket['id']}: {str(e)}")
success = False
return success
except Exception as e:
logger.error(f"Error creating storage buckets: {str(e)}")
return False
class SchoolUserCreator(UserCreator):
def __init__(self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, school_node, worker_node=None):
super().__init__(user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date)
self.school_node = school_node
self.worker_node = worker_node
def create_user(self):
# Ensure Neontology is initialized
logger.debug(f"Initializing Neontology connection. Closing any existing connection")
neon.close_neontology_connection()
logger.debug(f"Neontology connection closed. Initializing new connection")
neon.init_neontology_connection()
if self.user_type in ['email_teacher', 'ms_teacher']:
worker_node = self.create_teacher_node()
elif self.user_type in ['email_student', 'ms_student']:
worker_node = self.create_student_node()
else:
raise ValueError(f"User type {self.user_type} not supported")
self.user_nodes[f'worker_node'] = worker_node
user_node = self.create_user_node(self.cc_users_db_name)
logger.info(f"User node created: {user_node}")
self.user_nodes['default_user_node'] = user_node
self.create_user_worker_relationship(user_node, worker_node)
self.create_worker_school_relationship(worker_node, self.school_node)
logger.info(f"Worker school relationship created between {worker_node} and {self.school_node}")
return self.user_nodes
def create_teacher_node(self):
logger.debug(f"Teacher node will be created for school: {self.school_node}")
try:
return self._create_teacher_node()
except KeyError as ke:
raise ValueError(f"Missing required key in worker_data: {ke}") from ke
except Exception as e:
raise ValueError(f"Error creating teacher node: {e}") from e
def _create_teacher_node(self):
teacher_node = worker_nodes.TeacherNode(
unique_id=f"{self.user_id}",
tldraw_snapshot="",
worker_name=self.worker_name,
worker_email=self.worker_email,
worker_db_name=self.worker_db_name,
worker_type=self.worker_type
)
# Use the school's private database name if available
school_db = self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{self.school_node.id}"
logger.info(f"Teacher node template created: {teacher_node}... setting school db to {school_db}")
neon.create_or_merge_neontology_node(node=teacher_node, database=school_db, operation='merge')
logger.info(f"Teacher node merged into database {school_db}: {teacher_node}")
return teacher_node
def create_student_node(self):
student_node = worker_nodes.StudentNode(
unique_id=f"Student_{self.user_id}",
worker_name=self.worker_name,
worker_email=self.worker_email,
worker_db_name=self.worker_db_name,
worker_type=self.worker_type,
tldraw_snapshot=""
)
# Use the school's private database name if available
school_db = self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{self.school_node.id}"
logger.info(f"Student node template created: {student_node}... setting school db to {school_db}")
neon.create_or_merge_neontology_node(node=student_node, database=school_db, operation='merge')
logger.info(f"Student node merged into database {school_db}: {student_node}")
return student_node
def create_user_worker_relationship(self, user_node, worker_node):
user_role_rel = entity_relationships.UserIsSchoolWorker(source=user_node, target=worker_node)
# Use the school's private database name if available
school_db = self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{self.school_node.id}"
neon.create_or_merge_neontology_relationship(user_role_rel, database=school_db, operation='merge')
logger.info(f"Relationship created between user and worker in database {school_db}")
def create_worker_school_relationship(self, worker_node, school_node):
worker_school_rel = entity_relationships.EntityBelongsToSchool(source=worker_node, target=school_node)
# Use the school's private database name if available
school_db = school_node.private_database_name if hasattr(school_node, 'private_database_name') else f"cc.institutes.{school_node.school_type}.{school_node.id}"
neon.create_or_merge_neontology_relationship(worker_school_rel, database=school_db, operation='merge')
logger.info(f"Relationship created between worker and school in database {school_db}")
class NonSchoolUserCreator(UserCreator):
def __init__(self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, developer_role: str = "developer"):
super().__init__(user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date)
self.developer_role = developer_role
def create_user(self, access_token: Optional[str] = None):
logger.debug(f"Creating user node for {self.user_type} user {self.cc_username} in database {self.cc_users_db_name}")
# Create storage buckets for the user
user_bucket_id = self.user_db_name
user_bucket_name = f"{self.user_type.title()} User Files - {self.user_name}"
if not self.create_storage_bucket(user_bucket_id, user_bucket_name, access_token=access_token):
logger.error(f"Failed to create storage bucket for user {self.cc_username}")
raise ValueError(f"Failed to create storage bucket for user {self.cc_username}")
# Create default user node first
default_user_node = self.create_user_node(self.cc_users_db_name)
logger.debug(f"Default user node created: {default_user_node}")
# Verify the return value of create_user_node
if default_user_node is None:
logger.error("Failed to create default user node. It is None.")
raise ValueError("Failed to create default user node. It is None.")
self.user_nodes[f'default_user_node'] = default_user_node
# Create the appropriate user db based on user_type
if self.user_type == 'admin':
logger.debug(f"Creating super admin db for {self.user_type} user {self.cc_username} in database {self.user_db_name}")
self.create_super_admin_db()
elif self.user_type == 'developer':
logger.debug(f"Creating developer db for {self.user_type} user {self.cc_username} in database {self.user_db_name}")
self.create_developer_db()
else:
raise ValueError(f"User type {self.user_type} not supported")
logger.debug(f"User nodes after creation: {self.user_nodes}")
return self.user_nodes
def create_super_admin_db(self):
logger.debug(f"Creating super admin db for {self.user_type} user {self.cc_username} in database {self.user_db_name}")
neon.init_neontology_connection()
# Create the user db self.user_db_name
create_and_check_db(self.user_db_name)
try:
# Create the user node again for the user db
logger.debug(f"Creating super admin user node for {self.user_type} user {self.cc_username} in database {self.user_db_name}")
private_user_node = self.create_user_node(self.user_db_name)
super_admin_node = worker_nodes.SuperAdminNode(
unique_id=f"SuperAdmin_{self.user_id}",
worker_email=self.worker_email,
tldraw_snapshot="",
worker_name=self.worker_name,
worker_db_name=self.worker_db_name,
worker_type=self.worker_type
)
logger.debug(f"Super admin node template created: {super_admin_node}. Writing to database {self.user_db_name}")
neon.create_or_merge_neontology_node(node=super_admin_node, database=self.user_db_name, operation='merge')
logger.info(f"Super admin node created: {super_admin_node}")
logger.debug(f"Creating relationship between user node: {private_user_node} and worker node: {super_admin_node}")
self.create_user_specific_relationship(user_node=private_user_node, worker_node=super_admin_node)
logger.debug(f"Creating calendar for {self.user_type} user {self.cc_username} in database {self.user_db_name}")
calendar_nodes = self.create_calendar(user_node=private_user_node)
logger.info(f"Super admin calendar created.")
self.user_nodes['private_user_node'] = private_user_node
self.user_nodes['worker_node'] = super_admin_node
logger.info(f"Returning user nodes: {self.user_nodes}")
return self.user_nodes
except Exception as e:
logger.error(f"Error creating super admin node: {e}")
raise ValueError(f"Error creating super admin node: {e}") from e
def create_developer_db(self):
neon.init_neontology_connection()
# Create the user db self.user_db_name
create_and_check_db(self.user_db_name)
try:
# Create the user node again for the user db
private_user_node = self.create_user_node(self.user_db_name)
developer_node = worker_nodes.DeveloperNode(
unique_id=f"Developer_{self.user_id}",
worker_name=self.worker_name,
worker_email=self.worker_email,
tldraw_snapshot="",
worker_db_name=self.worker_db_name,
worker_type=self.worker_type,
developer_role=self.developer_role
)
neon.create_or_merge_neontology_node(developer_node, database=self.user_db_name, operation='merge')
logger.info(f"Developer node created: {developer_node}")
self.user_nodes['private_user_node'] = private_user_node
self.user_nodes['worker_node'] = developer_node
self.create_user_specific_relationship(user_node=private_user_node, worker_node=developer_node)
return self.user_nodes
except Exception as e:
raise ValueError(f"Error creating developer node: {e}") from e
def create_user_specific_relationship(self, user_node: user_nodes.UserNode, worker_node: Union[worker_nodes.SuperAdminNode, worker_nodes.DeveloperNode]):
if user_node is None or worker_node is None:
logger.error("User node or worker node is None. Cannot create relationship.")
raise ValueError("User node or worker node is None. Cannot create relationship.")
logger.info(f"Creating relationship between user node: {user_node} and worker node: {worker_node}")
# Log the state of user_node and worker_node
logger.debug(f"user_node: {user_node}")
logger.debug(f"worker_node: {worker_node}")
if worker_node.worker_type == 'developer':
specific_user_rel = entity_relationships.UserIsSystemWorker(source=user_node, target=worker_node)
elif worker_node.worker_type == 'superadmin':
specific_user_rel = entity_relationships.UserIsSystemWorker(source=user_node, target=worker_node)
else:
raise ValueError(f"User type {worker_node.worker_type} not supported")
neon.create_or_merge_neontology_relationship(specific_user_rel, database=self.user_db_name, operation='merge')
logger.info("Relationship created between user and specific node")
def create_calendar(self, user_node: user_nodes.UserNode):
calendar_nodes = init_calendar.create_calendar(self.user_db_name, self.calendar_start_date, self.calendar_end_date, attach_to_calendar_node=True, owner_node=user_node)
logger.info(f"Calendar nodes created.")
return calendar_nodes
@@ -0,0 +1,326 @@
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
)
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_unique_id: str, worker_unique_id: str) -> list:
"""
Retrieve all classes for a worker from the school database.
"""
query = """
MATCH (w:Teacher {unique_id: $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_unique_id)
classes = [record['c'] for record in result]
if not classes:
logger.warning(f"No classes found for teacher {worker_unique_id} in school database")
return classes
def get_school_class_periods(school_db_name: str, class_unique_id: str) -> list:
"""
Retrieve all periods for a class from the school database.
"""
query = """
MATCH (c:SubjectClass {unique_id: $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_unique_id)
periods = [record['l'] for record in result]
if not periods:
logger.warning(f"No periods found for class {class_unique_id} 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 {unique_id: $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.unique_id 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.unique_id)
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.unique_id}")
return []
if stats['year_count'] == 0:
logger.error(f"No calendar years found for user {user_node.unique_id}")
return []
if stats['month_count'] == 0:
logger.error(f"No calendar months found for user {user_node.unique_id}")
return []
if stats['day_count'] == 0:
logger.error(f"No calendar days found for user {user_node.unique_id}")
return []
# Get all calendar days without year filter
query = """
MATCH (w:User {unique_id: $user_id})-[:HAS_CALENDAR]->(c:Calendar)
-[:CALENDAR_INCLUDES_YEAR]->(y:CalendarYear)
-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth)
-[:MONTH_INCLUDES_DAY]->(d:CalendarDay)
RETURN d.unique_id as unique_id,
d.date as date,
d.day_of_week as day_of_week,
d.iso_day as iso_day,
d.path as path
ORDER BY d.date
"""
result = session.run(query, user_id=user_node.unique_id)
calendar_days = []
for record in result:
calendar_day = CalendarDayNode(
unique_id=record['unique_id'],
date=record['date'],
day_of_week=record['day_of_week'],
iso_day=record['iso_day'],
path=record['path']
)
calendar_days.append(calendar_day)
if not calendar_days:
logger.error(f"No calendar days found for user {user_node.unique_id}")
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.unique_id}")
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.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.unique_id}")
return {
"status": "error",
"message": "No calendar nodes found for user"
}
try:
# Create UserTeacherTimetableNode
timetable_unique_id = f"UserTeacherTimetable_{user_worker_node.teacher_code}"
worker_timetable = UserTeacherTimetableNode(
unique_id=timetable_unique_id,
school_db_name=school_db_name,
school_timetable_id=f"TeacherTimetable_{user_worker_node.teacher_code}",
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.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.unique_id, user_worker_node.unique_id)
if not school_classes:
logger.warning(f"No classes found for teacher {user_worker_node.unique_id} 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(
unique_id=class_data['unique_id'],
subject_class_code=class_data['subject_class_code'],
year_group=class_data['year_group'],
subject=class_data['subject'],
subject_code=class_data['subject_code'],
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.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['unique_id']] = []
# Get periods from school database
periods = get_school_class_periods(school_db_name, class_data['unique_id'])
if not periods:
logger.warning(f"No periods found for class {class_data['unique_id']} in school database")
continue
for period_data in periods:
# Create UserTimetableLessonNode
lesson_unique_id = f"UserTimetableLesson_{timetable_unique_id}_{class_name_safe}_{period_data['date']}_{period_data['period_code']}"
timetable_lesson_node = UserTimetableLessonNode(
unique_id=lesson_unique_id,
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['unique_id'],
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.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.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['unique_id']].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.unique_id != next_lesson.unique_id:
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)}"
}
@@ -0,0 +1,241 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_modules_database_init_init_worker_timetable'
log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback
logging = logger.get_logger(
name=log_name,
log_level=os.getenv("LOG_LEVEL", "DEBUG"),
log_path=log_dir,
log_file=log_name,
runtime=True,
log_format='default'
)
import pandas as pd
import re
import modules.database.tools.neo4j_driver_tools as driver
import modules.database.tools.neontology_tools as neon
import modules.database.tools.neo4j_session_tools as session
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
from modules.database.schemas.nodes.schools.schools import SubjectClassNode
from modules.database.schemas.nodes.workers.workers import TeacherNode
from modules.database.schemas.nodes.schools.timetable import AcademicPeriodNode, RegistrationPeriodNode
from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode
from modules.database.schemas.nodes.schools.pastoral import YearGroupSyllabusNode
from modules.database.schemas.relationships.planning_relationships import TimetableLessonBelongsToPeriod, TimetableLessonHasPlannedLesson, TeacherHasTimetable, TimetableHasClass, ClassHasLesson, TimetableLessonFollowsTimetableLesson, PlannedLessonFollowsPlannedLesson, SubjectClassBelongsToYearGroupSyllabus
def init_worker_timetable(timetable_df: pd.DataFrame, school_worker_node: TeacherNode):
logging.info(f"School worker node: {school_worker_node}")
worker_node = TeacherNode(**school_worker_node)
logging.info(f"Worker node: {worker_node}")
worker_db_name = worker_node.worker_db_name
logging.info(f"Initialising filesystem handler...")
fs_handler = ClassroomCopilotFilesystem(db_name=worker_db_name, init_run_type="user")
_, worker_timetable_path = fs_handler.create_teacher_timetable_directory(worker_node.path)
logging.info(f"Initialising neo4j connection...")
neon.init_neontology_connection()
try:
timetable_unique_id = f"TeacherTimetable_{worker_node.teacher_code}"
worker_timetable = TeacherTimetableNode(
unique_id=timetable_unique_id,
path=worker_timetable_path
)
neon.create_or_merge_neontology_node(worker_timetable, database=worker_db_name, operation='merge')
fs_handler.create_default_tldraw_file(worker_timetable.path, worker_timetable.to_dict())
neon.create_or_merge_neontology_relationship(
TeacherHasTimetable(source=worker_node, target=worker_timetable),
database=worker_db_name, operation='merge'
)
logging.info(f"Teacher timetable node created: {worker_timetable}")
# Group the timetable by class
class_groups = timetable_df.groupby('Class')
for class_name, class_df in class_groups:
if pd.notna(class_name):
class_name_safe = re.sub(r'[^A-Za-z0-9_ ]+', '', class_name)
_, class_path = fs_handler.create_teacher_class_directory(worker_timetable.path, class_name_safe)
subject_class_node_unique_id = f"SubjectClass_{class_name}"
subject_class_node = SubjectClassNode(
unique_id=subject_class_node_unique_id,
subject_class_code=class_name,
year_group=str(int(class_df['YearGroup'].iloc[0])), # TODO: Hacky fix for the year group being a float
subject=str(class_df['Subject'].iloc[0]),
subject_code=str(class_df['SubjectCode'].iloc[0]),
path=class_path
)
neon.create_or_merge_neontology_node(subject_class_node, database=worker_db_name, operation='merge')
logging.info(f"Class node created: {subject_class_node}")
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(subject_class_node.path, subject_class_node.to_dict())
# Link ClassNode to TeacherTimetableNode
neon.create_or_merge_neontology_relationship(
TimetableHasClass(source=worker_timetable, target=subject_class_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Relationship created from {worker_timetable.unique_id} to {subject_class_node.unique_id}")
# Link class to corresponding YearGoupSyllabus
year_group_syllabus_search_driver = driver.get_driver(worker_db_name)
year_group_syllabus_search_session = year_group_syllabus_search_driver.session(database=worker_db_name)
year_group_syllabus = session.find_nodes_by_label_and_properties(year_group_syllabus_search_session, "YearGroupSyllabus", {"yr_syllabus_year_group": subject_class_node.year_group, "yr_syllabus_subject_code": subject_class_node.subject_code})
if year_group_syllabus:
year_group_syllabus_node_data = year_group_syllabus[0]
year_group_syllabus_node = YearGroupSyllabusNode(**year_group_syllabus_node_data)
neon.create_or_merge_neontology_relationship(
SubjectClassBelongsToYearGroupSyllabus(source=subject_class_node, target=year_group_syllabus_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Relationship created from {subject_class_node.unique_id} to {year_group_syllabus_node.unique_id}")
else:
logging.warning(f"No YearGroupSyllabus found for class {class_name} with year group {subject_class_node.year_group} and subject code {subject_class_node.subject_code}")
class_lesson_nodes = []
planned_lesson_nodes = []
lesson_number = 0
for _, row in class_df.iterrows():
properties = {
"period_code": row['PeriodCode']
}
class_lessons_search_driver = driver.get_driver(worker_db_name)
class_lessons_search_session = class_lessons_search_driver.session(database=worker_db_name)
# If the period code contains "Rg" then we want to find the corresponding registration period and use its unique id
if "Rg" in row['PeriodCode']: # TODO: This is hacky and not very flexible. We are assuming that any period code containing "Rg" is a registration period. We should probably find a more robust way to identify registration periods
logging.info(f"Registration period found for class {class_name} with period code {row['PeriodCode']}")
class_lessons = session.find_nodes_by_label_and_properties(class_lessons_search_session, "RegistrationPeriod", properties)
else:
logging.info(f"Academic period found for class {class_name} with period code {row['PeriodCode']}")
class_lessons = session.find_nodes_by_label_and_properties(class_lessons_search_session, "AcademicPeriod", properties)
if class_lessons:
lesson_of_same_period = 0
number_of_lessons = len(class_lessons)
while lesson_of_same_period < number_of_lessons:
class_lesson = class_lessons[lesson_of_same_period]
if "Rg" in row['PeriodCode']:
period_node = RegistrationPeriodNode(**class_lesson)
else:
period_node = AcademicPeriodNode(**class_lesson)
lesson_period_code = row['PeriodCode']
date = class_lesson['date']
date_safe = date.strftime("%Y-%m-%d")
# Clean the class_name to make it directory-safe (catch all for invalid characters)
timetable_lesson_unique_id = f"TimetableLesson_{timetable_unique_id}_Class_{class_name}_Lesson_{lesson_number}_{date_safe}_{lesson_period_code}"
timetable_lesson_node = TimetableLessonNode(
unique_id=timetable_lesson_unique_id,
subject_class=class_name,
date=date,
start_time=class_lesson['start_time'].time(), # TODO: This is probably how we should format the start and end time properties for all such nodes
end_time=class_lesson['end_time'].time(),
period_code=lesson_period_code,
path="Not set"
)
neon.create_or_merge_neontology_node(timetable_lesson_node, database=worker_db_name, operation='merge')
logging.info(f"TimetableLessonNode created: {timetable_lesson_node}")
class_lesson_nodes.append(timetable_lesson_node)
neon.create_or_merge_neontology_relationship(
TimetableLessonBelongsToPeriod(source=timetable_lesson_node, target=period_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Relationship created from {timetable_lesson_node.unique_id} to {period_node.unique_id}")
# Link TimetableLessonNode to ClassNode
neon.create_or_merge_neontology_relationship(
ClassHasLesson(source=subject_class_node, target=timetable_lesson_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Relationship created from {subject_class_node.unique_id} to {timetable_lesson_node.unique_id}")
# Create PlannedLessonNode
planned_lesson_unique_id = f"PlannedLesson_{timetable_unique_id}_Class_{class_name}_Lesson_{lesson_number}_{date_safe}_{lesson_period_code}"
planned_lesson_node = PlannedLessonNode(
unique_id=planned_lesson_unique_id,
date=date,
start_time=class_lesson['start_time'].time(),
end_time=class_lesson['end_time'].time(),
period_code=lesson_period_code,
subject_class=class_name,
year_group=subject_class_node.year_group,
subject=subject_class_node.subject,
teacher_code=worker_node.teacher_code,
planning_status="Unplanned",
topic_code=None,
topic_name=None,
lesson_code=None,
lesson_name=None,
learning_statement_codes=None,
learning_statements=None,
learning_resource_codes=None,
learning_resources=None,
path="Not set"
)
# Create the PlannedLessonNode
neon.create_or_merge_neontology_node(planned_lesson_node, database=worker_db_name, operation='merge')
logging.info(f"PlannedLessonNode created: {planned_lesson_node}")
planned_lesson_nodes.append(planned_lesson_node)
# Link PlannedLessonNode to TimetableLessonNode
neon.create_or_merge_neontology_relationship(
TimetableLessonHasPlannedLesson(source=timetable_lesson_node, target=planned_lesson_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Relationship created from {timetable_lesson_node.unique_id} to {planned_lesson_node.unique_id}")
lesson_of_same_period += 1
lesson_number += 1
else:
logging.warning(f"No class periods found for class {class_name} on day {row['DayOfWeek']}")
# Sort the nodes by date and start time
class_lesson_nodes.sort(key=lambda x: (x.date, x.start_time))
planned_lesson_nodes.sort(key=lambda x: (x.date, x.start_time))
# Create sequential relationships and directories for TimetableLessonNodes
for i in range(1, len(class_lesson_nodes)):
previous_node = class_lesson_nodes[i - 1]
current_node = class_lesson_nodes[i]
i_safe = f"{i:02d}"
_, class_lesson_path = fs_handler.create_teacher_timetable_lesson_directory(class_path, f"{i_safe}_{current_node.date}_{current_node.period_code}")
current_node.path = class_lesson_path
neon.create_or_merge_neontology_node(current_node, database=worker_db_name, operation='merge')
logging.info(f"TimetableLessonNode directory created and node merged into database: {current_node}")
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(current_node.path, current_node.to_dict())
if previous_node:
neon.create_or_merge_neontology_relationship(
TimetableLessonFollowsTimetableLesson(source=previous_node, target=current_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Sequential relationship created between {previous_node.unique_id} and {current_node.unique_id}")
# Create sequential relationships for PlannedLessonNodes
for i in range(1, len(planned_lesson_nodes)):
previous_node = planned_lesson_nodes[i - 1]
current_node = planned_lesson_nodes[i]
i_safe = f"{i:02d}"
_, planned_lesson_path = fs_handler.create_teacher_planned_lesson_directory(class_path, f"{i_safe}_{current_node.date}_{current_node.period_code}")
current_node.path = planned_lesson_path
neon.create_or_merge_neontology_node(current_node, database=worker_db_name, operation='merge')
logging.info(f"PlannedLessonNode directory created and node merged into database: {current_node}")
# Create the tldraw file for the node
fs_handler.create_default_tldraw_file(current_node.path, current_node.to_dict())
if previous_node:
neon.create_or_merge_neontology_relationship(
PlannedLessonFollowsPlannedLesson(source=previous_node, target=current_node),
database=worker_db_name, operation='merge'
)
logging.info(f"Sequential relationship created between {previous_node.unique_id} and {current_node.unique_id}")
logging.info(f"Successfully initialized worker timetable for worker {worker_node.teacher_code}")
return {"status": "success", "message": "Worker timetable initialized successfully"}
except Exception as e:
logging.error(f"Error initializing worker timetable: {str(e)}")
return {"status": "error", "message": f"Error initializing worker timetable: {str(e)}"}
+35
View File
@@ -0,0 +1,35 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_modules_database_tools_xl_tools'
log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback
logging = logger.get_logger(
name=log_name,
log_level=os.getenv("LOG_LEVEL", "DEBUG"),
log_path=log_dir,
log_file=log_name,
runtime=True,
log_format='default'
)
import pandas as pd
from fastapi import UploadFile
def create_dataframes(excel_file, return_clean=False):
excel_sheets = pd.read_excel(excel_file, sheet_name=None)
# Log the sheet names
logging.info(f"Sheet names: {excel_sheets.keys()}")
return {sheet.lower(): data for sheet, data in excel_sheets.items()}
def create_dataframes_from_fastapiuploadfile(upload_file: UploadFile):
from io import BytesIO
file_content = upload_file.file.read()
file_content_io = BytesIO(file_content)
return pd.read_excel(file_content_io, sheet_name=None, engine='openpyxl')
def replace_nan_with_default(data, default_values):
for key in default_values:
if pd.isna(data.get(key, None)):
# logging.debug(f"Replacing NaN in {key} with default value '{default_values[key]}'")
data[key] = default_values[key]
return data
View File
+221
View File
@@ -0,0 +1,221 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
from modules.database.tools.neontology.graphconnection import GraphConnection, init_neontology
from modules.database.tools.neontology.basenode import BaseNode
from modules.database.tools.neontology.baserelationship import BaseRelationship
from typing import Optional, Dict, Any, List
from neo4j import Record as Neo4jRecord
import re
log_name = 'api_modules_database_neo4j_utils_neontology'
log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback
logging = logger.get_logger(
name=log_name,
log_level=os.getenv("LOG_LEVEL", "DEBUG"),
log_path=log_dir,
log_file=log_name,
runtime=True,
log_format='default'
)
class NeontologyProvider:
"""Provider class for managing Neontology connections and operations."""
def __init__(self):
"""Initialize the provider with Neo4j connection details from environment."""
self.bolt_url = os.getenv("APP_BOLT_URL")
self.user = os.getenv("USER_NEO4J")
self.password = os.getenv("PASSWORD_NEO4J")
self.connection = None
self.current_database = None
def _validate_database_name(self, database: str) -> str:
"""Validate and format database name to handle special characters."""
if not database:
raise ValueError("Database name cannot be empty")
if not re.match(r'^[a-zA-Z0-9_\.]+$', database):
raise ValueError("Database name contains invalid characters")
if database.count('.') > 1:
parts = database.split('.')
if len(parts) > 2:
formatted_name = f"{parts[0]}.{'.'.join(parts[1:])}"
logging.info(f"Reformatted database name from {database} to {formatted_name}")
return formatted_name
return database
def connect(self, database: str = 'neo4j') -> None:
"""Establish connection to Neo4j using Neontology."""
try:
formatted_database = self._validate_database_name(database)
if self.current_database != formatted_database and self.connection is not None:
self.close()
if self.connection is None:
init_neontology(
neo4j_uri=self.bolt_url,
neo4j_username=self.user,
neo4j_password=self.password
)
self.connection = GraphConnection()
self.current_database = formatted_database
logging.info(f"Neontology connection initialized with host: {self.host}, port: {self.port}, database: {formatted_database}")
except Exception as e:
logging.error(f"Failed to initialize Neontology connection: {str(e)}")
raise
def list_databases(self) -> List[str]:
"""List all available Neo4j databases."""
try:
with self.connection.driver.session() as session:
result = session.run("SHOW DATABASES")
return [record["name"] for record in result]
except Exception as e:
logging.error(f"Error listing databases: {str(e)}")
raise
def check_database_exists(self, database_name: str) -> bool:
"""Check if a specific database exists."""
try:
databases = self.list_databases()
return database_name in databases
except Exception as e:
logging.error(f"Error checking database existence: {str(e)}")
return False
def create_database(self, database_name: str) -> None:
"""Create a new Neo4j database."""
try:
formatted_name = self._validate_database_name(database_name)
with self.connection.driver.session() as session:
session.run(f"CREATE DATABASE {formatted_name} IF NOT EXISTS")
logging.info(f"Created database: {formatted_name}")
except Exception as e:
logging.error(f"Error creating database: {str(e)}")
raise
def reset_connection(self) -> None:
"""Reset the connection, forcing a new one to be created on next use."""
if self.connection:
self.close()
def create_or_merge_node(self, node: BaseNode, database: str = 'neo4j', operation: str = "merge") -> None:
"""Create or merge a node in the Neo4j database."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
if operation == "create":
node.create(database=database)
elif operation == "merge":
node.merge(database=database)
else:
logging.error(f"Invalid operation: {operation}")
raise ValueError(f"Invalid operation: {operation}")
except Exception as e:
logging.error(f"Error in processing node: {e}")
raise
def create_or_merge_relationship(self, relationship: BaseRelationship, database: str = 'neo4j', operation: str = "merge") -> None:
"""Create or merge a relationship in the Neo4j database."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
if operation == "create":
relationship.create(database=database)
elif operation == "merge":
relationship.merge(database=database)
else:
logging.error(f"Invalid operation: {operation}")
raise ValueError(f"Invalid operation: {operation}")
except Exception as e:
logging.error(f"Error in processing relationship: {e}")
raise
def cypher_write(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> None:
"""Execute a write transaction."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
self.connection.cypher_write(cypher, params)
except Exception as e:
logging.error(f"Error in cypher write: {e}")
raise
def cypher_read(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> Optional[Neo4jRecord]:
"""Execute a read transaction returning a single record."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
return self.connection.cypher_read(cypher, params)
except Exception as e:
logging.error(f"Error in cypher read: {e}")
raise
def cypher_read_many(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Neo4jRecord]:
"""Execute a read transaction returning multiple records."""
try:
if not self.connection or self.current_database != database:
self.connect(database)
return self.connection.cypher_read_many(cypher, params)
except Exception as e:
logging.error(f"Error in cypher read many: {e}")
raise
def run_query(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Dict[str, Any]]:
"""
Execute a Cypher query and return results as a list of dictionaries.
This is a convenience method that handles both single and multiple record results.
Args:
cypher: The Cypher query to execute
params: Query parameters
database: Target database name
Returns:
List[Dict[str, Any]]: Query results as a list of dictionaries
"""
try:
if not self.connection or self.current_database != database:
self.connect(database)
# Use cypher_read_many for consistent return type
records = self.connection.cypher_read_many(cypher, params)
# Convert Neo4j records to dictionaries
results = []
for record in records:
# Handle both Record and dict types
if isinstance(record, Neo4jRecord):
results.append(dict(record))
else:
results.append(record)
return results
except Exception as e:
logging.error(f"Error in run_query: {e}")
raise
def close(self) -> None:
"""Close the Neontology connection."""
if self.connection:
# The connection will be closed when the GraphConnection instance is deleted
self.connection = None
self.current_database = None
logging.info("Neontology connection closed")
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.close()
+29
View File
@@ -0,0 +1,29 @@
from typing import Union
import modules.database.schemas.nodes.users as user_schemas
import modules.database.schemas.nodes.workers.workers as worker_schemas
import modules.database.schemas.nodes.schools.schools as school_schemas
user_entities = [
user_schemas.UserNode
]
worker_entities = Union[
worker_schemas.TeacherNode,
worker_schemas.StudentNode,
worker_schemas.SchoolAdminNode,
worker_schemas.DeveloperNode,
worker_schemas.SuperAdminNode,
]
school_entities = Union[
school_schemas.SchoolNode,
school_schemas.DepartmentNode,
school_schemas.SubjectClassNode,
school_schemas.RoomNode,
]
entities = Union[
user_entities,
worker_entities,
school_entities,
]
@@ -0,0 +1,52 @@
from typing import ClassVar
from modules.database.tools.neontology.basenode import BaseNode
class CCBaseNode(BaseNode):
__primarylabel__: ClassVar[str] = ''
__primaryproperty__: ClassVar[str] = 'unique_id'
unique_id: str
tldraw_snapshot: str
def to_dict(self):
return {
"__primarylabel__": self.__primarylabel__,
"unique_id": self.unique_id,
"tldraw_snapshot": self.tldraw_snapshot,
}
class UserBaseNode(CCBaseNode):
__primarylabel__: ClassVar[str] = ''
cc_username: str
user_email: str
user_db_name: str
user_name: str
user_type: str
def to_dict(self):
return {
"__primarylabel__": self.__primarylabel__,
"unique_id": self.unique_id,
"tldraw_snapshot": self.tldraw_snapshot,
"cc_username": self.cc_username,
"user_db_name": self.user_db_name,
"user_email": self.user_email,
"user_name": self.user_name,
"user_type": self.user_type,
}
class WorkerBaseNode(CCBaseNode):
__primarylabel__: ClassVar[str] = ''
worker_name: str
worker_email: str
worker_db_name: str
worker_type: str
def to_dict(self):
return {
"__primarylabel__": self.__primarylabel__,
"unique_id": self.unique_id,
"tldraw_snapshot": self.tldraw_snapshot,
"worker_name": self.worker_name,
"worker_email": self.worker_email,
"worker_db_name": self.worker_db_name,
"worker_type": self.worker_type,
}
@@ -0,0 +1,36 @@
import datetime
from typing import ClassVar
from .base_nodes import CCBaseNode
class CalendarNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'Calendar'
name: str
start_date: datetime.date
end_date: datetime.date
class CalendarYearNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'CalendarYear'
year: str
class CalendarMonthNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'CalendarMonth'
year: str
month: str
month_name: str
class CalendarWeekNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'CalendarWeek'
start_date: datetime.date
week_number: str
iso_week: str # ISO 8601 week
class CalendarDayNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'CalendarDay'
date: datetime.date
day_of_week: str
iso_day: str # ISO 8601 day
class CalendarTimeChunkNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'CalendarTimeChunk'
start_time: datetime.time
end_time: datetime.time
@@ -0,0 +1,56 @@
from typing import ClassVar, Optional
from ..base_nodes import CCBaseNode
class CurriculumBaseNode(CCBaseNode):
__primarylabel__: ClassVar[str] = ''
class KeyStageNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'KeyStage'
key_stage: str
name: str
class KeyStageSyllabusNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'KeyStageSyllabus'
id: str
name: str
key_stage: str
subject_name: str
class SubjectNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'Subject'
id: str
name: str
class TopicNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'Topic'
id: str
name: str
type: str
assessment_type: str
total_number_of_lessons_for_topic: str
class TopicLessonNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'TopicLesson'
id: str
name: str
type: str
length: Optional[str] = None
suggested_activities: Optional[str] = None
skills_learned: Optional[str] = None
weblinks: Optional[str] = None
class LearningStatementNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'LearningStatement'
id: str
name: str
type: str
class ScienceLabNode(CurriculumBaseNode):
__primarylabel__: ClassVar[str] = 'ScienceLab'
id: str
name: str
summary: Optional[str] = None
requirements: Optional[str] = None
procedure: Optional[str] = None
safety: Optional[str] = None
weblinks: Optional[str] = None
@@ -0,0 +1,14 @@
from typing import ClassVar, Optional
from ..base_nodes import CCBaseNode
class YearGroupNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'YearGroup'
year_group: str
name: str
class YearGroupSyllabusNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'YearGroupSyllabus'
id: str
name: str
year_group: str
subject_name: str
@@ -0,0 +1,60 @@
from typing import ClassVar, Optional
from ..base_nodes import CCBaseNode
class SchoolNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'School'
# Core identification fields (required for all databases)
id: str # School's unique identifier within its type
school_type: str # e.g., 'development', 'state', 'private', etc.
name: str
website: str = 'unknown'
# Public school fields (required for public database)
statutory_low_age: Optional[int] = None
statutory_high_age: Optional[int] = None
phase_of_education: Optional[str] = None
school_capacity: Optional[int] = None
religious_character: Optional[str] = None
ofsted_rating: Optional[str] = None
# Private school fields (required for private database)
establishment_number: Optional[str] = None
establishment_name: Optional[str] = None
establishment_type: Optional[str] = None
establishment_status: Optional[str] = None
@property
def database_id(self) -> str:
"""Get the full database identifier for this school"""
return f"{self.school_type}.{self.id}"
@property
def public_database_name(self) -> str:
"""Get the public database name for this school"""
return "cc.institutes"
@property
def private_database_name(self) -> str:
"""Get the private database name for this school"""
return f"cc.institutes.{self.database_id}"
@property
def curriculum_database_name(self) -> str:
"""Get the curriculum database name for this school"""
return f"{self.private_database_name}.curriculum"
class DepartmentNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'Department'
name: str
class SubjectClassNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'SubjectClass'
name: Optional[str] = 'unknown'
year_group_id: str
subject_id: str
class RoomNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'Room'
name: Optional[str] = 'unknown'
building_id: Optional[str] = 'unknown'
@@ -0,0 +1,88 @@
import datetime
from typing import ClassVar
from ..base_nodes import CCBaseNode
class SchoolTimetableNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'SchoolTimetable'
school_timetable_id: str
start_date: datetime.date
end_date: datetime.date
class AcademicYearNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'AcademicYear'
year: str
class AcademicTermNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'AcademicTerm'
term_name: str
term_number: str
start_date: datetime.date
end_date: datetime.date
class AcademicTermBreakNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'AcademicTermBreak'
term_break_name: str
start_date: datetime.date
end_date: datetime.date
class AcademicWeekNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'AcademicWeek'
academic_week_number: str
start_date: datetime.date
week_type: str
class HolidayWeekNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'HolidayWeek'
start_date: datetime.date
class AcademicDayNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'AcademicDay'
academic_day: str
date: datetime.date
day_of_week: str
day_type: str
class OffTimetableDayNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'OffTimetableDay'
date: datetime.date
day_of_week: str
class StaffDayNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'StaffDay'
date: datetime.date
day_of_week: str
class HolidayDayNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'HolidayDay'
date: datetime.date
day_of_week: str
class AcademicPeriodNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'AcademicPeriod'
name: str
date: datetime.date
start_time: datetime.datetime
end_time: datetime.datetime
period_code: str
class RegistrationPeriodNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'RegistrationPeriod'
name: str
date: datetime.date
start_time: datetime.datetime
end_time: datetime.datetime
period_code: str
class BreakPeriodNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'BreakPeriod'
name: str
date: datetime.date
start_time: datetime.datetime
end_time: datetime.datetime
class OffTimetablePeriodNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'OffTimetablePeriod'
name: str
date: datetime.date
start_time: datetime.datetime
end_time: datetime.datetime
@@ -0,0 +1,23 @@
from typing import ClassVar
from ..base_nodes import CCBaseNode
class DepartmentStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'DepartmentStructure'
class PastoralStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'PastoralStructure'
class CurriculumStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'CurriculumStructure'
class SiteStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'SiteStructure'
class StaffStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'StaffStructure'
class StudentStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'StudentStructure'
class ITAdminStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'ITAdminStructure'
@@ -0,0 +1,8 @@
from typing import ClassVar
from ..base_nodes import CCBaseNode
class WorkerStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'WorkerStructure'
class UserCalendarStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'UserCalendarStructure'
@@ -0,0 +1,5 @@
from typing import ClassVar, Optional
from ..base_nodes import CCBaseNode
class WorkerTimetableStructureNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'WorkerTimetableStructure'
+5
View File
@@ -0,0 +1,5 @@
from typing import ClassVar
from .base_nodes import UserBaseNode
class UserNode(UserBaseNode):
__primarylabel__: ClassVar[str] = 'User'
@@ -0,0 +1,63 @@
import datetime
from typing import ClassVar, Optional, List, Union
from ..base_nodes import CCBaseNode
class EntityTimetableBaseNode(CCBaseNode):
start_date: datetime.date
end_date: datetime.date
class WorkerTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'WorkerTimetable'
worker_timetable_id: str
class TeacherTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'TeacherTimetable'
teacher_timetable_id: str
class UserTeacherTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'UserTeacherTimetable'
user_teacher_timetable_id: str
class StudentTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'StudentTimetable'
student_timetable_id: str
class SchoolAdminTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'SchoolAdminTimetable'
school_admin_timetable_id: str
class DeveloperTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'DeveloperTimetable'
developer_timetable_id: str
class SuperAdminTimetableNode(EntityTimetableBaseNode):
__primarylabel__: ClassVar[str] = 'SuperAdminTimetable'
super_admin_timetable_id: str
class TimetableLessonNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'TimetableLesson'
subject_class: str
date: datetime.date
start_time: datetime.time
end_time: datetime.time
period_code: str
class PlannedLessonNode(CCBaseNode):
__primarylabel__: ClassVar[str] = 'PlannedLesson'
date: datetime.date
start_time: datetime.time
end_time: datetime.time
period_code: str
subject_class: str
year_group: str
subject: str
teacher_code: str
planning_status: str
topic_code: Optional[str] = None
topic_name: Optional[str] = None
lesson_code: Optional[str] = None
lesson_name: Optional[str] = None
learning_statement_codes: Optional[List[str]] = None
learning_statements: Optional[List[str]] = None
learning_resource_codes: Optional[List[str]] = None
learning_resources: Optional[List[str]] = None
@@ -0,0 +1,19 @@
import datetime
from typing import ClassVar
from ..base_nodes import WorkerBaseNode
class SchoolAdminNode(WorkerBaseNode):
__primarylabel__: ClassVar[str] = 'SchoolAdmin'
class TeacherNode(WorkerBaseNode):
__primarylabel__: ClassVar[str] = 'Teacher'
class StudentNode(WorkerBaseNode):
__primarylabel__: ClassVar[str] = 'Student'
class DeveloperNode(WorkerBaseNode):
__primarylabel__: ClassVar[str] = 'Developer'
developer_role: str # To distinguish between admin/developer roles
class SuperAdminNode(WorkerBaseNode):
__primarylabel__: ClassVar[str] = 'SuperAdmin'
+29
View File
@@ -0,0 +1,29 @@
from typing import Union
import modules.database.schemas.nodes.users as user_schemas
import modules.database.schemas.nodes.workers.workers as worker_schemas
import modules.database.schemas.nodes.schools.schools as school_schemas
user_owners = Union[
user_schemas.UserNode
]
worker_owners = Union[
worker_schemas.TeacherNode,
worker_schemas.StudentNode,
worker_schemas.SchoolAdminNode,
worker_schemas.DeveloperNode,
worker_schemas.SuperAdminNode,
]
school_owners = Union[
school_schemas.SchoolNode,
school_schemas.DepartmentNode,
school_schemas.SubjectClassNode,
school_schemas.RoomNode,
]
owners = Union[
user_owners,
worker_owners,
school_owners,
]
@@ -0,0 +1,29 @@
import modules.database.schemas.nodes.calendars as calendar_schemas
from modules.database.tools.neontology.baserelationship import BaseRelationship
from typing import ClassVar
# Sequenced Relationships for Calendar
class YearFollowsYear(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_FOLLOWS_YEAR'
source: calendar_schemas.CalendarYearNode
target: calendar_schemas.CalendarYearNode
class MonthFollowsMonth(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'MONTH_FOLLOWS_MONTH'
source: calendar_schemas.CalendarMonthNode
target: calendar_schemas.CalendarMonthNode
class WeekFollowsWeek(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'WEEK_FOLLOWS_WEEK'
source: calendar_schemas.CalendarWeekNode
target: calendar_schemas.CalendarWeekNode
class DayFollowsDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'DAY_FOLLOWS_DAY'
source: calendar_schemas.CalendarDayNode
target: calendar_schemas.CalendarDayNode
class TimeChunkFollowsTimeChunk(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TIME_CHUNK_FOLLOWS_TIME_CHUNK'
source: calendar_schemas.CalendarTimeChunkNode
target: calendar_schemas.CalendarTimeChunkNode
@@ -0,0 +1,96 @@
import modules.database.schemas.nodes.schools.timetable as neo_timetable
import modules.database.schemas.nodes.calendars as neo_calendar
import modules.database.schemas.nodes.workers.timetable as neo_teacher_timetable
from modules.database.tools.neontology.baserelationship import BaseRelationship
from typing import ClassVar
class CalendarYearIsAcademicYear(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_ACADEMIC_YEAR'
source: neo_calendar.CalendarYearNode
target: neo_timetable.AcademicYearNode
class AcademicYearIsCalendarYear(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_YEAR'
source: neo_timetable.AcademicYearNode
target: neo_calendar.CalendarYearNode
class CalendarWeekIsAcademicWeek(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_ACADEMIC_WEEK'
source: neo_calendar.CalendarWeekNode
target: neo_timetable.AcademicWeekNode
class AcademicWeekIsCalendarWeek(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_WEEK'
source: neo_timetable.AcademicWeekNode
target: neo_calendar.CalendarWeekNode
class CalendarWeekIsHolidayWeek(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_HOLIDAY_WEEK'
source: neo_calendar.CalendarWeekNode
target: neo_timetable.HolidayWeekNode
class HolidayWeekIsCalendarWeek(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_WEEK'
source: neo_timetable.HolidayWeekNode
target: neo_calendar.CalendarWeekNode
class CalendarDayIsAcademicDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_ACADEMIC_DAY'
source: neo_calendar.CalendarDayNode
target: neo_timetable.AcademicDayNode
class AcademicDayIsCalendarDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY'
source: neo_timetable.AcademicDayNode
target: neo_calendar.CalendarDayNode
class CalendarDayIsHolidayDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_HOLIDAY_DAY'
source: neo_calendar.CalendarDayNode
target: neo_timetable.HolidayDayNode
class HolidayDayIsCalendarDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY'
source: neo_timetable.HolidayDayNode
target: neo_calendar.CalendarDayNode
class CalendarDayIsOffTimetableDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_OFF_TIMETABLE_DAY'
source: neo_calendar.CalendarDayNode
target: neo_timetable.OffTimetableDayNode
class OffTimetableDayIsCalendarDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY'
source: neo_timetable.OffTimetableDayNode
target: neo_calendar.CalendarDayNode
class CalendarDayIsStaffDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_STAFF_DAY'
source: neo_calendar.CalendarDayNode
target: neo_timetable.StaffDayNode
class StaffDayIsCalendarDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY'
source: neo_timetable.StaffDayNode
target: neo_calendar.CalendarDayNode
# New relationships for user timetable
class CalendarDayHasTimetableLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_TIMETABLE_LESSON'
source: neo_calendar.CalendarDayNode
target: neo_teacher_timetable.TimetableLessonNode
class TimetableLessonBelongsToCalendarDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'BELONGS_TO_CALENDAR_DAY'
source: neo_teacher_timetable.TimetableLessonNode
target: neo_calendar.CalendarDayNode
class CalendarDayHasPlannedLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_PLANNED_LESSON'
source: neo_calendar.CalendarDayNode
target: neo_teacher_timetable.PlannedLessonNode
class PlannedLessonBelongsToCalendarDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'BELONGS_TO_CALENDAR_DAY'
source: neo_teacher_timetable.PlannedLessonNode
target: neo_calendar.CalendarDayNode
@@ -0,0 +1,34 @@
import modules.database.schemas.nodes.calendars as calendar_schemas
from modules.database.tools.neontology.baserelationship import BaseRelationship
from typing import ClassVar
## Calendar layer relationships
class CalendarIncludesYear(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'CALENDAR_INCLUDES_YEAR'
source: calendar_schemas.CalendarNode
target: calendar_schemas.CalendarYearNode
class YearIncludesMonth(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_INCLUDES_MONTH'
source: calendar_schemas.CalendarYearNode
target: calendar_schemas.CalendarMonthNode
class YearIncludesWeek(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_INCLUDES_WEEK'
source: calendar_schemas.CalendarYearNode
target: calendar_schemas.CalendarWeekNode
class MonthIncludesDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'MONTH_INCLUDES_DAY'
source: calendar_schemas.CalendarMonthNode
target: calendar_schemas.CalendarDayNode
class WeekIncludesDay(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'WEEK_INCLUDES_DAY'
source: calendar_schemas.CalendarWeekNode
target: calendar_schemas.CalendarDayNode
class DayIncludesTimeChunk(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'DAY_INCLUDES_TIME_CHUNK'
source: calendar_schemas.CalendarDayNode
target: calendar_schemas.CalendarTimeChunkNode
@@ -0,0 +1,119 @@
from typing import ClassVar
from modules.database.tools.neontology.baserelationship import BaseRelationship
import modules.database.schemas.nodes.schools.pastoral as pastoral_nodes
import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes
import modules.database.schemas.nodes.structures.schools as school_structures
# Structure layer relationships
class CurriculumStructureIncludesKeyStage(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'CURRICULUM_STRUCTURE_INCLUDES_KEY_STAGE'
source: school_structures.CurriculumStructureNode
target: curriculum_nodes.KeyStageNode
class PastoralStructureIncludesYearGroup(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'PASTORAL_STRUCTURE_INCLUDES_YEAR_GROUP'
source: school_structures.PastoralStructureNode
target: pastoral_nodes.YearGroupNode
## Curriculum layer relationships
class SubjectForKeyStage(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'SUBJECT_FOR_KEY_STAGE'
source: curriculum_nodes.SubjectNode
target: curriculum_nodes.KeyStageNode
class SubjectHasYearGroupSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'SUBJECT_HAS_YEAR_GROUP_SYLLABUS'
source: curriculum_nodes.SubjectNode
target: pastoral_nodes.YearGroupSyllabusNode
class SubjectHasKeyStageSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'SUBJECT_HAS_KEY_STAGE_SYLLABUS'
source: curriculum_nodes.SubjectNode
target: curriculum_nodes.KeyStageSyllabusNode
class TopicPartOfYearGroupSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_SYLLABUS_INCLUDES_TOPIC'
source: pastoral_nodes.YearGroupSyllabusNode
target: curriculum_nodes.TopicNode
class KeyStageSyllabusIncludesTopic(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'KEY_STAGE_SYLLABUS_INCLUDES_TOPIC'
source: curriculum_nodes.KeyStageSyllabusNode
target: curriculum_nodes.TopicNode
class YearGroupHasYearGroupSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_GROUP_HAS_YEAR_GROUP_SYLLABUS'
source: pastoral_nodes.YearGroupNode
target: pastoral_nodes.YearGroupSyllabusNode
class KeyStageSyllabusIncludesYearGroupSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'KEY_STAGE_SYLLABUS_INCLUDES_YEAR_GROUP_SYLLABUS'
source: curriculum_nodes.KeyStageSyllabusNode
target: pastoral_nodes.YearGroupSyllabusNode
class KeyStageIncludesKeyStageSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'KEY_STAGE_INCLUDES_KEY_STAGE_SYLLABUS'
source: curriculum_nodes.KeyStageNode
target: curriculum_nodes.KeyStageSyllabusNode
class KeyStageIncludesTopic(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'KEY_STAGE_INCLUDES_TOPIC'
source: curriculum_nodes.KeyStageNode
target: curriculum_nodes.TopicNode
class SubjectIncludesTopic(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'SUBJECT_INCLUDES_TOPIC'
source: curriculum_nodes.SubjectNode
target: curriculum_nodes.TopicNode
class TopicIncludesTopicLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TOPIC_INCLUDES_LESSON'
source: curriculum_nodes.TopicNode
target: curriculum_nodes.TopicLessonNode
class TopicIncludesLearningStatement(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TOPIC_INCLUDES_LEARNING_STATEMENT'
source: curriculum_nodes.TopicNode
target: curriculum_nodes.LearningStatementNode
class LessonIncludesLearningStatement(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'LESSON_INCLUDES_LEARNING_STATEMENT'
source: curriculum_nodes.TopicLessonNode
target: curriculum_nodes.LearningStatementNode
# Science-specific curriculum layer relationships
class TopicLessonIncludesScienceLab(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'LESSON_INCLUDES_SCIENCE_LAB'
source: curriculum_nodes.TopicLessonNode
target: curriculum_nodes.ScienceLabNode
class KeyStageFollowsKeyStage(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'KEY_STAGE_FOLLOWS_KEY_STAGE'
source: curriculum_nodes.KeyStageNode
target: curriculum_nodes.KeyStageNode
class YearGroupFollowsYearGroup(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_GROUP_FOLLOWS_YEAR_GROUP'
source: pastoral_nodes.YearGroupNode
target: pastoral_nodes.YearGroupNode
class KeyStageSyllabusFollowsKeyStageSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'KEY_STAGE_SYLLABUS_FOLLOWS_KEY_STAGE_SYLLABUS'
source: curriculum_nodes.KeyStageSyllabusNode
target: curriculum_nodes.KeyStageSyllabusNode
class YearGroupSyllabusFollowsYearGroupSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_GROUP_SYLLABUS_FOLLOWS_YEAR_GROUP_SYLLABUS'
source: pastoral_nodes.YearGroupSyllabusNode
target: pastoral_nodes.YearGroupSyllabusNode
class TopicLessonFollowsTopicLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'LESSON_FOLLOWS_LESSON'
source: curriculum_nodes.TopicLessonNode
target: curriculum_nodes.TopicLessonNode
class YearGroupSyllabusIncludesTopic(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'YEAR_GROUP_SYLLABUS_INCLUDES_TOPIC'
source: pastoral_nodes.YearGroupSyllabusNode
target: curriculum_nodes.TopicNode
@@ -0,0 +1,15 @@
from typing import ClassVar
from modules.database.tools.neontology.baserelationship import BaseRelationship
import modules.database.schemas.nodes.schools.schools as school_nodes
import modules.database.schemas.nodes.structures.schools as school_structures
class SchoolHasCurriculumStructure(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_CURRICULUM_STRUCTURE'
source: school_nodes.SchoolNode
target: school_structures.CurriculumStructureNode
class SchoolHasPastoralStructure(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_PASTORAL_STRUCTURE'
source: school_nodes.SchoolNode
target: school_structures.PastoralStructureNode
@@ -0,0 +1,49 @@
from typing import ClassVar, Union
import modules.database.schemas.entities as entities
import modules.database.schemas.nodes.users as user_nodes
import modules.database.schemas.nodes.workers.workers as worker_nodes
import modules.database.schemas.nodes.schools.schools as school_nodes
import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes
import modules.database.schemas.nodes.structures.schools as school_structures
from modules.database.tools.neontology.baserelationship import BaseRelationship
class UserIsSchoolWorker(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_SCHOOL_WORKER'
source: user_nodes.UserNode
target: Union[worker_nodes.SchoolAdminNode, worker_nodes.TeacherNode, worker_nodes.StudentNode]
class UserIsSystemWorker(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'IS_SYSTEM_WORKER'
source: user_nodes.UserNode
target: Union[worker_nodes.DeveloperNode, worker_nodes.SuperAdminNode]
class EntityBelongsToSchool(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'BELONGS_TO'
source: Union[entities.worker_entities, entities.school_entities]
target: entities.school_entities
class EntityBelongsToDepartment(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'BELONGS_TO'
source: entities.worker_entities
target: school_nodes.DepartmentNode
class SchoolHasDepartmentStructure(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_DEPARTMENT_STRUCTURE'
source: school_nodes.SchoolNode
target: school_structures.DepartmentStructureNode
class DepartmentStructureHasDepartment(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_DEPARTMENT'
source: school_structures.DepartmentStructureNode
target: school_nodes.DepartmentNode
class DepartmentManagesKeyStageSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'MANAGES_KEY_STAGE_SYLLABUS'
source: school_nodes.DepartmentNode
target: curriculum_nodes.KeyStageSyllabusNode
class DepartmentManagesSubject(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'MANAGES_SUBJECT'
source: school_nodes.DepartmentNode
target: curriculum_nodes.SubjectNode
@@ -0,0 +1,18 @@
from typing import ClassVar, Union
from modules.database.tools.neontology.baserelationship import BaseRelationship
import modules.database.schemas.nodes.users as user_nodes
import modules.database.schemas.nodes.workers.workers as worker_nodes
import modules.database.schemas.nodes.schools.schools as school_nodes
import modules.database.schemas.nodes.workers.timetable as worker_timetable
import modules.database.schemas.nodes.schools.timetable as school_timetable
class EntityHasTimetable(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_TIMETABLE'
source: Union[user_nodes.UserNode, worker_nodes.TeacherNode, worker_nodes.StudentNode, school_nodes.SubjectClassNode]
target: worker_timetable.WorkerTimetableNode
class SchoolHasTimetable(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_TIMETABLE'
source: school_nodes.SchoolNode
target: school_timetable.SchoolTimetableNode
@@ -0,0 +1,9 @@
from typing import ClassVar, Union
import modules.database.schemas.nodes.calendars as calendar_schemas
import modules.database.schemas.owners as owner_schemas
from modules.database.tools.neontology.baserelationship import BaseRelationship
class OwnerHasCalendar(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'HAS_CALENDAR'
source: owner_schemas.owners
target: calendar_schemas.CalendarNode
@@ -0,0 +1,48 @@
from typing import ClassVar, Union
from modules.database.tools.neontology.baserelationship import BaseRelationship
from modules.database.schemas.nodes.schools.timetable import AcademicPeriodNode, RegistrationPeriodNode
from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode
from modules.database.schemas.nodes.workers.workers import TeacherNode
from modules.database.schemas.nodes.schools.schools import SubjectClassNode
from modules.database.schemas.nodes.schools.pastoral import YearGroupSyllabusNode
class TimetableLessonBelongsToPeriod(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'LESSON_BELONGS_TO_PERIOD'
source: TimetableLessonNode
target: Union[AcademicPeriodNode, RegistrationPeriodNode]
class TimetableLessonHasPlannedLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TIMETABLE_LESSON_HAS_PLANNED_LESSON'
source: TimetableLessonNode
target: PlannedLessonNode
class TeacherHasTimetable(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TEACHER_HAS_TIMETABLE'
source: TeacherNode
target: TeacherTimetableNode
class TimetableHasClass(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TIMETABLE_HAS_CLASS'
source: TeacherTimetableNode
target: SubjectClassNode
class ClassHasLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'CLASS_HAS_LESSON'
source: SubjectClassNode
target: TimetableLessonNode
class TimetableLessonFollowsTimetableLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'TIMETABLE_LESSON_FOLLOWS_TIMETABLE_LESSON'
source: Union[TimetableLessonNode, PlannedLessonNode]
target: Union[TimetableLessonNode, PlannedLessonNode]
class PlannedLessonFollowsPlannedLesson(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'PLANNED_LESSON_FOLLOWS_PLANNED_LESSON'
source: PlannedLessonNode
target: PlannedLessonNode
class SubjectClassBelongsToYearGroupSyllabus(BaseRelationship):
__relationshiptype__: ClassVar[str] = 'SUBJECT_CLASS_BELONGS_TO_YEAR_GROUP_SYLLABUS'
source: SubjectClassNode
target: YearGroupSyllabusNode

Some files were not shown because too many files have changed in this diff Show More