Initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
from modules.logger_tool import initialise_logger
|
||||
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
|
||||
|
||||
from fastapi import Request, HTTPException, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
import jwt
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
import os
|
||||
|
||||
class SupabaseBearer(HTTPBearer):
|
||||
def __init__(self, auto_error: bool = True):
|
||||
super().__init__(auto_error=auto_error)
|
||||
|
||||
async def __call__(self, credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
|
||||
if not credentials:
|
||||
raise HTTPException(status_code=403, detail="Invalid authorization code.")
|
||||
|
||||
try:
|
||||
token = credentials.credentials
|
||||
payload = verify_supabase_token(token)
|
||||
return payload
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification failed: {str(e)}")
|
||||
raise HTTPException(status_code=403, detail="Invalid token or expired token.")
|
||||
|
||||
def verify_supabase_token(token: str) -> dict:
|
||||
"""Verify a Supabase JWT token and return its payload."""
|
||||
try:
|
||||
jwt_secret = os.getenv("JWT_SECRET")
|
||||
if not jwt_secret:
|
||||
raise ValueError("JWT_SECRET not configured")
|
||||
|
||||
# Decode the token with proper audience check
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
jwt_secret,
|
||||
algorithms=["HS256"],
|
||||
audience="authenticated"
|
||||
)
|
||||
|
||||
logger.debug(f"Token payload: {payload}")
|
||||
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.error("Token has expired")
|
||||
raise HTTPException(status_code=401, detail="Token has expired")
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.error(f"Invalid token: {str(e)}")
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification failed: {str(e)}")
|
||||
raise HTTPException(status_code=401, detail="Token verification failed")
|
||||
|
||||
def decodeSupabaseJWT(token: str) -> dict:
|
||||
try:
|
||||
jwt_secret = os.getenv('SUPABASE_JWT_SECRET')
|
||||
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"], audience="authenticated")
|
||||
return payload
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Initialize the security instance
|
||||
security = HTTPBearer()
|
||||
|
||||
async def verify_supabase_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
try:
|
||||
token = credentials.credentials
|
||||
# Verify token using your Supabase JWT secret
|
||||
decoded_token = jwt.decode(
|
||||
token,
|
||||
os.getenv('SUPABASE_JWT_SECRET'),
|
||||
algorithms=["HS256"],
|
||||
audience="authenticated"
|
||||
)
|
||||
return decoded_token
|
||||
except InvalidTokenError as e:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail=f"Invalid authentication token: {str(e)}"
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}"}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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'
|
||||
@@ -0,0 +1,5 @@
|
||||
from typing import ClassVar
|
||||
from .base_nodes import UserBaseNode
|
||||
|
||||
class UserNode(UserBaseNode):
|
||||
__primarylabel__: ClassVar[str] = 'User'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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'
|
||||
@@ -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,
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user