Initial commit

This commit is contained in:
2025-07-11 13:52:19 +00:00
commit e0c489f625
362 changed files with 27286 additions and 0 deletions
View File
+4
View File
@@ -0,0 +1,4 @@
from fastapi import APIRouter, Depends, File, UploadFile
from backend.app.run.dependencies import admin_dependency
router = APIRouter()
+14
View File
@@ -0,0 +1,14 @@
from fastapi import APIRouter, Depends
from backend.app.run.dependencies import admin_dependency
import modules.database.tools.neo4j_driver_tools as driver
import modules.database.tools.neo4j_session_tools as session
import modules.database.tools.neo4j_http_tools as http
import modules.database.tools.queries as query
router = APIRouter()
# Handle neo4j driver
@router.post("/create-driver")
async def create_driver(driver: driver.Neo4jDriver = Depends(driver.get_neo4j_driver)):
return driver
View File
+32
View File
@@ -0,0 +1,32 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_init_calendar'
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'
)
from modules.database.tools.neontology.basenode import BaseNode
import modules.database.init.init_calendar as init_calendar
from fastapi import APIRouter
from datetime import date
from fastapi import HTTPException
router = APIRouter()
@router.post("/create-calendar")
async def create_calendar(db_name: str, start_date: date, end_date: date, attach_to_calendar_node: bool = False, entity_node: BaseNode = None):
try:
logging.info(f"Creating calendar for {db_name} from {start_date} to {end_date}")
if entity_node is None:
logging.info("No user entity node provided, proceeding without attaching to user entity.")
return init_calendar.create_calendar(db_name, start_date, end_date, attach_to_calendar_node, entity_node)
except Exception as e:
logging.error(f"Error processing request: {e}")
raise HTTPException(status_code=422, detail=str(e))
+16
View File
@@ -0,0 +1,16 @@
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 APIRouter, File, UploadFile, Form, BackgroundTasks
router = APIRouter()
@router.post("/upload-class-list")
async def upload_class_list(
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
user_node: str = Form(...),
worker_node: str = Form(...)
):
pass
+50
View File
@@ -0,0 +1,50 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_init_curriculum'
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 modules.database.init.xl_tools as xl
import modules.database.init.init_school_curriculum as init_school_curriculum
from modules.database.schemas.nodes.schools.schools import SchoolNode
from fastapi import APIRouter, File, UploadFile, Form
router = APIRouter()
@router.post("/upload-curriculum")
async def upload_curriculum(file: UploadFile = File(...), db_name: str = Form(...)):
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return {"status": "Error", "message": "Invalid file format"}
logging.info(f"Uploading curriculum for {db_name}")
dataframes = xl.create_dataframes_from_fastapiuploadfile(file)
return init_school_curriculum.create_curriculum(db_name, dataframes)
@router.post("/upload-school-curriculum")
async def upload_school_curriculum(
file: UploadFile = File(...),
db_name: str = Form(...),
school_uuid: str = Form(...),
school_name: str = Form(...),
school_website: str = Form(...),
school_path: str = Form(...)
):
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return {"status": "Error", "message": "Invalid file format"}
logging.info(f"Uploading curriculum for school {school_name} in {db_name}")
dataframes = xl.create_dataframes_from_fastapiuploadfile(file)
school_node = SchoolNode(
unique_id=f'School_{school_uuid}',
school_uuid=school_uuid,
school_name=school_name,
school_website=school_website,
path=school_path
)
return init_school_curriculum.create_curriculum(db_name, dataframes, school_node)
+280
View File
@@ -0,0 +1,280 @@
import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
import modules.database.tools.neo4j_driver_tools as driver_tools
import modules.database.tools.neo4j_session_tools as session_tools
import modules.database.init.init_user as init_user
from modules.database.tools.neo4j_db_formatter import format_user_email_for_neo_db
import modules.database.init.init_school as init_school
import modules.database.init.init_school_timetable as init_school_timetable
import modules.database.init.init_school_curriculum as init_school_curriculum
import modules.database.init.xl_tools as xl
from modules.database.schemas.nodes.schools.schools import SchoolNode, SubjectClassNode, RoomNode, DepartmentNode
from fastapi import APIRouter, Form, HTTPException
from fastapi.responses import JSONResponse
import json
VALID_USER_TYPES = ['admin', 'cc_admin', 'cc_email_school_admin', 'cc_ms_school_admin', 'email_school_admin', 'ms_school_admin', 'cc_email_teacher', 'cc_ms_teacher', 'cc_email_student', 'cc_ms_student', 'email_teacher', 'ms_teacher', 'email_student', 'ms_student', 'ms_federated_teacher', 'ms_federated_student', 'standard', 'developer'] # TODO: Implement dev_ user types for pytests, consider use of cc_ user types
router = APIRouter()
# Helpers
def initialise_schools_from_config():
"""Initialize a school with the configuration provided from env variables
"""
default_config = {
"school_uuid": "kevlarai",
"school_name": "KevlarAI School",
"school_website": "https://kevlarai.com",
"timetable_file": "kevlarai_data/kevlarai_timetable.xlsx",
"curriculum_file": "kevlarai_data/kevlarai_curriculum.xlsx"
}
# school_config_str = os.getenv("SCHOOL_CONFIG") # TODO: Implement this
school_config = default_config
db_name = f"cc.institutes.{school_config['school_uuid']}"
curriculum_db_name = f"{db_name}.curriculum"
logger.info(f"Creating database for {school_config['school_name']} using db_name: {db_name}")
driver = driver_tools.get_driver()
if driver is None:
logger.error("Failed to connect to Neo4j")
return
with driver.session() as session:
# Create main school database
session_tools.create_database(session, db_name)
logger.debug(f"Database {db_name} created")
# Create curriculum database
session_tools.create_database(session, curriculum_db_name)
logger.debug(f"Curriculum database {curriculum_db_name} created")
# Add filesystem path debugging
base_path = os.getenv("NODE_FILESYSTEM_PATH")
schools_path = os.path.join(base_path, "schools")
school_path = os.path.join(schools_path, f"cc.institutes.{school_config['school_uuid']}")
logger.debug("Filesystem paths:", {
"base_path": base_path,
"schools_path": schools_path,
"school_path": school_path
})
# Check if directories exist
logger.debug("Directory existence check:", {
"base_exists": os.path.exists(base_path),
"schools_exists": os.path.exists(schools_path),
"school_exists": os.path.exists(school_path)
})
# Create database entry for school without timetable or curriculum
logger.info(f"Creating school entry for {school_config['school_name']} in database {db_name} without timetable or curriculum")
result = init_school.create_school(
db_name=db_name,
school_uuid=school_config["school_uuid"],
school_name=school_config["school_name"],
school_website=school_config["school_website"]
)
logger.success(f"{school_config['school_name']} school entry created successfully")
# Create school node from result
school_node = result['school_node']
refreshed_school_node = SchoolNode(
unique_id=school_node.unique_id,
school_uuid=school_node.school_uuid,
school_name=school_node.school_name,
school_website=school_node.school_website,
path=school_node.path
)
# Create timetable entries for school from Excel file
timetable_file = os.path.join(os.getenv("BACKEND_INIT_PATH"), school_config["timetable_file"])
logger.info(f"Creating timetable entries for {school_config['school_name']} using timetable file: {timetable_file}.")
school_timetable_dataframes = xl.create_dataframes(timetable_file)
init_school_timetable.create_school_timetable(
dataframes=school_timetable_dataframes,
db_name=db_name,
school_node=refreshed_school_node
)
logger.success("Timetable entries created successfully")
# Create curriculum entries for school from Excel file in both databases
curriculum_file = os.path.join(os.getenv("BACKEND_INIT_PATH"), school_config["curriculum_file"])
school_curriculum_dataframes = xl.create_dataframes(curriculum_file)
logger.info(f"Creating curriculum entries for {school_config['school_name']} using curriculum file: {curriculum_file}.")
init_school_curriculum.create_curriculum(
dataframes=school_curriculum_dataframes,
db_name=db_name,
curriculum_db_name=curriculum_db_name,
school_node=refreshed_school_node
)
logger.success("Curriculum entries created successfully")
@router.post("/create-user")
async def create_user(
user_id: str = Form(...),
user_type: str = Form(...),
user_name: str = Form(...),
user_email: str = Form(...),
school_uuid: str = Form(None),
school_name: str = Form(None),
school_website: str = Form(None),
school_path: str = Form(None),
worker_data: str = Form(None)
):
logger.info(f"Creating user with user_id: {user_id}, user_type: {user_type}, user_name: {user_name}, user_email: {user_email}")
if school_uuid:
logger.info(f"School UUID provided: {school_uuid}")
else:
logger.info(f"No school UUID provided")
if school_name:
logger.info(f"School name provided: {school_name}")
else:
logger.info(f"No school name provided")
if school_website:
logger.info(f"School website provided: {school_website}")
else:
logger.info(f"No school website provided")
if school_path:
logger.info(f"School path provided: {school_path}")
else:
logger.info(f"No school path provided")
if worker_data:
logger.info(f"Worker data provided: {worker_data}")
else:
logger.info(f"No worker data provided")
# Validate inputs
if any(param is None for param in (user_type, user_name, user_email, user_id)):
raise HTTPException(status_code=400, detail=f"Invalid user data")
if user_type not in VALID_USER_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid user type: {user_type}")
try:
# Parse worker data
worker_data_dict = json.loads(worker_data) if worker_data else None
# Create school node if school data provided
school_node = None
if all([school_uuid, school_name, school_website, school_path]):
school_node = SchoolNode(
unique_id=f'School_{school_uuid}',
school_uuid=school_uuid,
school_name=school_name,
school_website=school_website,
path=school_path
)
# Create user with single database reference
formatted_email = format_user_email_for_neo_db(user_email)
user_db_name = f"cc.users.{formatted_email}"
result = init_user.create_user(
db_name=user_db_name,
user_id=user_id,
user_type=user_type,
username=user_name,
email=user_email,
school_node=school_node,
worker_data=worker_data_dict
)
# Ensure the result is JSON serializable
response_data = {
"status": "success",
"data": {
"user_node": result['user_node'],
"worker_node": result['worker_node'],
"calendar_nodes": result.get('calendar_nodes')
}
}
return JSONResponse(content=response_data)
except Exception as e:
logger.error(f"Error creating user in Neo4j: {str(e)}", exc_info=True)
return JSONResponse(
content={"status": "error", "message": str(e)},
status_code=500
)
@router.post("/create-schools")
async def create_schools():
initialise_schools_from_config()
return JSONResponse(content={"status": "success", "message": "Schools created successfully"})
@router.post("/create-department")
async def create_department(
db_name: str = Form(...),
unique_id: str = Form(...),
department_name: str = Form(...),
department_code: str = Form(...),
path: str = Form(...)
):
if db_name is None or unique_id is None or department_name is None or department_code is None or path is None:
logging.error(f"Invalid department data: {db_name}, {unique_id}, {department_name}, {department_code}, {path}")
raise HTTPException(status_code=400, detail="Invalid department data")
department = DepartmentNode(
unique_id=unique_id,
department_name=department_name,
department_code=department_code,
path=path
)
logger.info(f"Creating department {department_name} with unique_id {unique_id}")
try:
result = init_school.create_department(db_name, department)
return JSONResponse(content={"status": "success", "data": result})
except Exception as e:
logger.error(f"Error creating department: {str(e)}")
return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500)
@router.post("/create-class")
async def create_class(
db_name: str = Form(...),
unique_id: str = Form(...),
subject_class_code: str = Form(...),
year_group: str = Form(...),
subject: str = Form(...),
subject_code: str = Form(...),
path: str = Form(...)
):
subject_class_node = SubjectClassNode(
unique_id=unique_id,
subject_class_code=subject_class_code,
year_group=year_group,
subject=subject,
subject_code=subject_code,
path=path
)
# Implementation for creating a class
pass
@router.post("/create-room")
async def create_room(
db_name: str = Form(...),
room_unique_id: str = Form(...),
room_code: str = Form(...),
path: str = Form(...)
):
room = RoomNode(
room_unique_id=room_unique_id,
room_code=room_code,
path=path
)
# Implementation for creating a room
pass
+28
View File
@@ -0,0 +1,28 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_init_get_data'
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 modules.database.init.xl_tools as xl
from fastapi import APIRouter, File, UploadFile
router = APIRouter()
@router.post("/get-dataframes-from-xl")
async def get_dataframes_from_xl(file: UploadFile = File(...)):
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return {"status": "Error", "message": "Invalid file format"}
try:
logging.info(f"Getting dataframes from {file.filename}")
return xl.create_dataframes(await file.read())
except Exception as e:
return {"status": "Error", "message": str(e)}
+115
View File
@@ -0,0 +1,115 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_init_schools'
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'
)
from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks
import pandas as pd
import modules.database.tools.neo4j_driver_tools as driver
from modules.database.tools.neo4j_session_tools import get_node_by_unique_id
import modules.database.init.init_school_timetable as init_school_timetable
import modules.database.init.init_worker_timetable as init_worker_timetable
from modules.database.schemas.nodes.schools.schools import SchoolNode
import modules.database.init.xl_tools as xl
import json
router = APIRouter()
@router.post("/upload-school-timetable")
async def upload_school_timetable(
file: UploadFile = File(...),
db_name: str = Form(...),
unique_id: str = Form(...),
school_uuid: str = Form(...),
school_name: str = Form(...),
school_website: str = Form(...),
path: str = Form(...)
):
school_node = SchoolNode(
unique_id=unique_id,
school_uuid=school_uuid,
school_name=school_name,
school_website=school_website,
path=path
)
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return {"status": "Error", "message": "Invalid file format"}
logging.info(f"Uploading timetable for {db_name} from {file.filename}")
dataframes = xl.create_dataframes_from_fastapiuploadfile(file)
return init_school_timetable.create_school_timetable(dataframes, db_name, school_node)
@router.post("/upload-worker-timetable")
async def upload_worker_timetable(
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
worker_node: str = Form(...)
):
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
raise HTTPException(status_code=422, detail="Invalid file format")
try:
worker_node_data = json.loads(worker_node)
logging.info(f"Uploading worker timetable for {worker_node_data['teacher_code']} from {file.filename} for {worker_node_data['worker_db_name']}")
logging.debug(f"Worker node data: {worker_node_data}")
# Read file content into memory
file_content = await file.read()
# Schedule the processing of the timetable in the background
background_tasks.add_task(
process_worker_timetable,
file_content,
worker_node_data
)
return {
"status": "Accepted",
"message": "Processing of teacher timetable started"
}
except Exception as e:
logging.error(f"Error handling timetable upload: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
async def process_worker_timetable(file_content, worker_node_data):
neo_driver = driver.get_driver(db_name=worker_node_data['worker_db_name'])
if neo_driver is None:
logging.error(f"Failed to connect to the database {worker_node_data['worker_db_name']}")
return
try:
# Create a DataFrame from the file content
from io import BytesIO
timetable_df = pd.read_excel(BytesIO(file_content))
# Get the school version of the worker node
logging.info(f"Getting school worker node for {worker_node_data['unique_id']} from {worker_node_data['worker_db_name']}")
with neo_driver.session(database=worker_node_data['worker_db_name']) as neo_session:
school_worker_node = get_node_by_unique_id(session=neo_session, unique_id=worker_node_data['unique_id'])
if school_worker_node is None:
error_msg = f"School worker node not found for unique_id: {worker_node_data['unique_id']}"
logging.error(error_msg)
raise Exception(error_msg)
logging.debug(f"School worker node found: {school_worker_node}")
logging.info(f"Initializing worker timetable for school worker: {school_worker_node['teacher_code']}")
init_worker_timetable.init_worker_timetable(timetable_df, school_worker_node)
logging.info(f"Worker timetable initialized for school worker: {school_worker_node['teacher_code']}")
except Exception as e:
logging.error(f"Error processing worker timetable: {str(e)}")
raise
finally:
logging.info(f"Closing driver for {worker_node_data['worker_db_name']}")
driver.close_driver(neo_driver)
+166
View File
@@ -0,0 +1,166 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_init_timetables'
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'
)
from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks
import pandas as pd
import modules.database.tools.neo4j_driver_tools as driver
from modules.database.tools.neo4j_session_tools import get_node_by_unique_id
import modules.database.init.init_school_timetable as init_school_timetable
import modules.database.init.init_worker_timetable as init_worker_timetable
from modules.database.schemas.nodes.users import UserNode
from modules.database.schemas.nodes.schools.schools import SchoolNode
from modules.database.schemas.nodes.workers.workers import TeacherNode
import modules.database.init.xl_tools as xl
import json
import modules.database.tools.neontology_tools as neon
router = APIRouter()
@router.post("/upload-school-timetable")
async def upload_school_timetable(
file: UploadFile = File(...),
db_name: str = Form(...),
unique_id: str = Form(...),
school_uuid: str = Form(...),
school_name: str = Form(...),
school_website: str = Form(...),
path: str = Form(...)
):
school_node = SchoolNode(
unique_id=unique_id,
school_uuid=school_uuid,
school_name=school_name,
school_website=school_website,
path=path
)
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return {"status": "Error", "message": "Invalid file format"}
logging.info(f"Uploading timetable for {db_name} from {file.filename}")
dataframes = xl.create_dataframes_from_fastapiuploadfile(file)
return init_school_timetable.create_school_timetable(dataframes, db_name, school_node)
@router.post("/upload-worker-timetable")
async def upload_worker_timetable(
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
user_node: str = Form(...),
worker_node: str = Form(...)
):
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
raise HTTPException(status_code=422, detail="Invalid file format")
try:
worker_node_data = json.loads(worker_node)
user_node_data = json.loads(user_node)
logging.info(f"Uploading worker timetable for {worker_node_data['teacher_code']} from {file.filename} for {worker_node_data['worker_db_name']}")
logging.debug(f"Worker node data: {worker_node_data}")
logging.debug(f"User node data: {user_node_data}")
# Read file content into memory
file_content = await file.read()
# Schedule the processing of the timetable in the background
background_tasks.add_task(
process_worker_timetable,
file_content,
user_node_data,
worker_node_data
)
return {
"status": "Accepted",
"message": "Processing of teacher timetable started"
}
except Exception as e:
logging.error(f"Error handling timetable upload: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
async def process_worker_timetable(file_content, user_node_data, worker_node_data):
# Initialize neontology connection first
neon.init_neontology_connection()
neo_driver = driver.get_driver(db_name=worker_node_data['worker_db_name'])
if neo_driver is None:
logging.error(f"Failed to connect to the database {worker_node_data['worker_db_name']}")
return
try:
# Create a DataFrame from the file content
from io import BytesIO
timetable_df = pd.read_excel(BytesIO(file_content))
# Get the school version of the worker node
logging.info(f"Getting school worker node for {worker_node_data['unique_id']} from {worker_node_data['worker_db_name']}")
with neo_driver.session(database=worker_node_data['worker_db_name']) as neo_session:
school_worker_node = get_node_by_unique_id(session=neo_session, unique_id=worker_node_data['unique_id'])
if school_worker_node is None:
error_msg = f"School worker node not found for unique_id: {worker_node_data['unique_id']}"
logging.error(error_msg)
raise Exception(error_msg)
logging.debug(f"School worker node found: {school_worker_node}")
# Create timetable in school database
logging.info(f"Initializing worker timetable for school worker: {school_worker_node['teacher_code']}")
init_worker_timetable.init_worker_timetable(timetable_df, school_worker_node)
logging.info(f"Worker timetable initialized for school worker: {school_worker_node['teacher_code']}")
# Create timetable in user database
if 'user_db_name' in worker_node_data:
from modules.database.init.init_user_timetable import create_user_worker_timetable
from modules.database.schemas.nodes.workers.workers import TeacherNode
logging.info(f"Creating user timetable structure in {worker_node_data['user_db_name']}")
# Create TeacherNode from worker_node_data
user_worker_node = TeacherNode(
unique_id=worker_node_data['unique_id'],
teacher_code=worker_node_data['teacher_code'],
teacher_name_formal=worker_node_data['teacher_name_formal'],
teacher_email=worker_node_data['teacher_email'],
path=worker_node_data['path'],
worker_db_name=worker_node_data['worker_db_name'],
user_db_name=worker_node_data['user_db_name']
)
# Create user node
user_node = UserNode(
unique_id=user_node_data['unique_id'],
user_id=user_node_data['user_id'],
user_type=user_node_data['user_type'],
user_name=user_node_data['user_name'],
user_email=user_node_data['user_email'],
path=user_node_data['path'],
worker_node_data=user_node_data['worker_node_data']
)
# Create user timetable structure
create_user_worker_timetable(
user_node=user_node,
user_worker_node=user_worker_node,
school_db_name=worker_node_data['worker_db_name']
)
logging.info(f"User timetable structure created in {worker_node_data['user_db_name']}")
else:
logging.warning("No user_db_name provided, skipping user timetable creation")
except Exception as e:
logging.error(f"Error processing worker timetable: {str(e)}")
raise
finally:
logging.info(f"Closing driver for {worker_node_data['worker_db_name']}")
driver.close_driver(neo_driver)
+99
View File
@@ -0,0 +1,99 @@
from fastapi import APIRouter, Depends, File, UploadFile
from backend.app.run.dependencies import admin_dependency
from pydantic import BaseModel
router = APIRouter()
class NodeBase(BaseModel):
Name: str
class LocalAuthority(NodeBase):
pass
class SchoolNode(NodeBase):
Type: str
Status: str
class ParliamentaryConstituency(NodeBase):
pass
class AdministrativeWard(NodeBase):
pass
class RelationshipBase(BaseModel):
start_node: NodeBase
end_node: NodeBase
relationship_type: str
class HasParliamentaryConstituency(RelationshipBase):
pass
class HasAdministrativeWard(RelationshipBase):
pass
class HasSchool(RelationshipBase):
pass
@router.post("/batch-create-schools")
async def add_school_to_global(file: UploadFile = File(...)):
if file is None:
return {"status": "Error", "message": "No file received"}
try:
import pandas as pd
from io import BytesIO
from app.modules.driver_tools import create_node_http, create_relationship_http
data = pd.read_csv(BytesIO(await file.read()), usecols=["LA (name)", "ParliamentaryConstituency (name)", "AdministrativeWard (name)", "EstablishmentName", "TypeOfEstablishment (name)", "EstablishmentStatus (name)"])
unique_las = data["LA (name)"].unique()
for la_name in unique_las:
la_node = {"Name": la_name}
la_id = create_node_http("LocalAuthority", la_node, db="GlobalSchools")
constituencies = data[data["LA (name)"] == la_name]["ParliamentaryConstituency (name)"].unique()
for constituency in constituencies:
constituency_node = {"Name": constituency}
constituency_id = create_node_http("ParliamentaryConstituency", constituency_node, db="GlobalSchools")
create_relationship_http({"start_node": {"id": la_id}, "end_node": {"id": constituency_id}, "relationship_type": "HAS_PARLIAMENTARY_CONSTITUENCY"}, db="GlobalSchools")
wards = data[(data["LA (name)"] == la_name) & (data["ParliamentaryConstituency (name)"] == constituency)]["AdministrativeWard (name)"].unique()
for ward in wards:
ward_node = {"Name": ward}
ward_id = create_node_http("AdministrativeWard", ward_node, db="GlobalSchools")
create_relationship_http({"start_node": {"id": constituency_id}, "end_node": {"id": ward_id}, "relationship_type": "HAS_ADMINISTRATIVE_WARD"}, db="GlobalSchools")
schools = data[(data["LA (name)"] == la_name) & (data["ParliamentaryConstituency (name)"] == constituency) & (data["AdministrativeWard (name)"] == ward)]
for index, school in schools.iterrows():
school_node = {
"Name": school["EstablishmentName"],
"Type": school["TypeOfEstablishment (name)"],
"Status": school["EstablishmentStatus (name)"]
}
school_id = create_node_http("School", school_node, db="GlobalSchools")
create_relationship_http({"start_node": {"id": ward_id}, "end_node": {"id": school_id}, "relationship_type": "HAS_SCHOOL"}, db="GlobalSchools")
return {"status": "Success", "message": "Graph structure updated successfully"}
except Exception as e:
print("Failed to process file:", e)
return {"status": "Error", "message": "Failed to process file"}
@router.post("/create-school")
async def add_school_to_global(file: UploadFile = File(...)):
if file is None:
return {"status": "Error", "message": "No file received"}
try:
import pandas as pd
from io import BytesIO
data = pd.read_excel(BytesIO(await file.read()), usecols=[0], nrows=5).squeeze()
print("Data read from file:", data)
if len(data) < 5:
return {"status": "Error", "message": "Insufficient data in file"}
school_data = {
"name": data[0],
"address": data[1],
"ofsted_number": data[2],
"website": data[3],
"geo_location": data[4]
}
from app.modules.driver_tools import create_node_http
response = create_node_http("globalschools", "School", school_data)
return {"status": "School added to global school db via HTTP", "school_data": school_data, "response": response}
except Exception as e:
print("Failed to process file:", e)
return {"status": "Error", "message": "Failed to process file"}
+5
View File
@@ -0,0 +1,5 @@
from fastapi import APIRouter, Depends, File, UploadFile
from backend.app.run.dependencies import admin_dependency
router = APIRouter()
+5
View File
@@ -0,0 +1,5 @@
from fastapi import APIRouter, Depends, File, UploadFile
from backend.app.run.dependencies import admin_dependency
router = APIRouter()
View File
@@ -0,0 +1,220 @@
import os
from fastapi import APIRouter, HTTPException
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from modules.logger_tool import initialise_logger
from modules.database.tools import neo4j_driver_tools as driver_tools
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
router = APIRouter()
@router.get("/get-calendar-structure")
async def get_calendar_structure(db_name: str) -> Dict[str, Any]:
"""
Get the complete calendar structure including years, months, weeks, and days.
"""
try:
# Get all calendar nodes in a single query
query = """
// Match all calendar-related nodes
MATCH (y:CalendarYear)
OPTIONAL MATCH (y)-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth)
OPTIONAL MATCH (m)-[:MONTH_INCLUDES_DAY]->(d:CalendarDay)
OPTIONAL MATCH (w:CalendarWeek)-[:WEEK_INCLUDES_DAY]->(d)
WITH y, m, w, d
ORDER BY y.date, m.date, w.date, d.date
// Collect all nodes with dates converted to strings
RETURN {
years: collect(DISTINCT {
id: y.unique_id,
path: y.path,
date: toString(y.date),
__primarylabel__: 'CalendarYear'
}),
months: collect(DISTINCT {
id: m.unique_id,
path: m.path,
date: toString(m.date),
__primarylabel__: 'CalendarMonth'
}),
weeks: collect(DISTINCT {
id: w.unique_id,
path: w.path,
date: toString(w.date),
__primarylabel__: 'CalendarWeek'
}),
days: collect(DISTINCT {
id: d.unique_id,
path: d.path,
date: toString(d.date),
week_id: w.unique_id,
month_id: m.unique_id,
__primarylabel__: 'CalendarDay'
})
} as structure
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
record = result.single()
if not record:
raise HTTPException(status_code=404, detail="Calendar structure not found")
structure = record["structure"]
# Find current day using string comparison
today = datetime.now().strftime("%Y-%m-%d")
current_day = next(
(day["id"] for day in structure["days"]
if day["date"] == today),
structure["days"][0]["id"] if structure["days"] else None
)
return {
"status": "success",
"structure": {
"years": structure["years"],
"months": structure["months"],
"weeks": structure["weeks"],
"days": structure["days"],
"currentDay": current_day
}
}
except Exception as e:
logger.error(f"Error getting calendar structure: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-calendar-days")
async def get_calendar_days(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]:
"""
Get all calendar days in a date range.
"""
try:
query = """
MATCH (d:CalendarDay)
WHERE date(d.date) >= date($start_date) AND date(d.date) <= date($end_date)
OPTIONAL MATCH (w:CalendarWeek)-[:WEEK_INCLUDES_DAY]->(d)
OPTIONAL MATCH (m:CalendarMonth)-[:MONTH_INCLUDES_DAY]->(d)
RETURN {
id: d.unique_id,
path: d.path,
date: d.date,
week_id: w.unique_id,
month_id: m.unique_id,
__primarylabel__: 'CalendarDay'
} as day
ORDER BY d.date
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, start_date=start_date, end_date=end_date)
days = [record["day"] for record in result]
return {
"status": "success",
"days": days
}
except Exception as e:
logger.error(f"Error getting calendar days: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-calendar-weeks")
async def get_calendar_weeks(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]:
"""
Get all calendar weeks in a date range.
"""
try:
query = """
MATCH (w:CalendarWeek)-[:WEEK_INCLUDES_DAY]->(d:CalendarDay)
WHERE date(w.date) >= date($start_date) AND date(w.date) <= date($end_date)
WITH w, collect(d) as days
RETURN {
id: w.unique_id,
path: w.path,
date: w.date,
day_ids: [day in days | day.unique_id],
__primarylabel__: 'CalendarWeek'
} as week
ORDER BY w.date
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, start_date=start_date, end_date=end_date)
weeks = [record["week"] for record in result]
return {
"status": "success",
"weeks": weeks
}
except Exception as e:
logger.error(f"Error getting calendar weeks: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-calendar-months")
async def get_calendar_months(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]:
"""
Get all calendar months in a date range.
"""
try:
query = """
MATCH (m:CalendarMonth)-[:MONTH_INCLUDES_DAY]->(d:CalendarDay)
WHERE date(m.date) >= date($start_date) AND date(m.date) <= date($end_date)
WITH m, collect(d) as days
RETURN {
id: m.unique_id,
path: m.path,
date: m.date,
day_ids: [day in days | day.unique_id],
__primarylabel__: 'CalendarMonth'
} as month
ORDER BY m.date
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, start_date=start_date, end_date=end_date)
months = [record["month"] for record in result]
return {
"status": "success",
"months": months
}
except Exception as e:
logger.error(f"Error getting calendar months: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-calendar-years")
async def get_calendar_years(db_name: str) -> Dict[str, Any]:
"""
Get all calendar years.
"""
try:
query = """
MATCH (y:CalendarYear)-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth)
WITH y, collect(m) as months
RETURN {
id: y.unique_id,
path: y.path,
date: y.date,
month_ids: [month in months | month.unique_id],
__primarylabel__: 'CalendarYear'
} as year
ORDER BY y.date
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
years = [record["year"] for record in result]
return {
"status": "success",
"years": years
}
except Exception as e:
logger.error(f"Error getting calendar years: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,257 @@
from fastapi import APIRouter, HTTPException
from typing import Dict, Any
from modules.database.tools import neo4j_driver_tools as driver_tools
from modules.logger_tool import initialise_logger
from neo4j.time import DateTime, Date
import os
from datetime import datetime
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
router = APIRouter()
def convert_neo4j_values(value: Any) -> Any:
"""Convert Neo4j types to JSON-serializable types."""
if isinstance(value, DateTime):
return value.isoformat() # Convert to ISO format string
elif isinstance(value, Date):
return value.isoformat() # Convert Date to ISO format string
elif isinstance(value, dict):
return {k: convert_neo4j_values(v) for k, v in value.items()}
elif isinstance(value, list):
return [convert_neo4j_values(v) for v in value]
return value
def get_default_node_week(db_name: str) -> Dict[str, Any]:
"""Get the current week node."""
# Get today's date
today = datetime.now()
# Find the calendar week node that contains today's date
query = """
MATCH (w:CalendarWeek)
WHERE date(w.start_date) <= date($today) AND date($today) <= date(w.start_date) + duration('P7D')
RETURN w
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, today=today.strftime('%Y-%m-%d'))
week_node = result.single()
if not week_node:
raise HTTPException(status_code=404, detail="No default node found for context: week")
node = week_node["w"]
node_data = dict(node)
converted_data = convert_neo4j_values(node_data)
return {
"status": "success",
"node": {
"id": node["unique_id"],
"path": node["path"],
"type": "CalendarWeek",
"label": node.get("title", "Calendar Week"),
"data": converted_data
}
}
def get_default_node_month(db_name: str) -> Dict[str, Any]:
"""Get the current month node."""
# Get today's date
today = datetime.now()
# Find the calendar month node for the current month
query = """
MATCH (m:CalendarMonth)
WHERE m.year = $year AND m.month = $month
RETURN m
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, year=str(today.year), month=str(today.month))
month_node = result.single()
if not month_node:
raise HTTPException(status_code=404, detail="No default node found for context: month")
node = month_node["m"]
node_data = dict(node)
converted_data = convert_neo4j_values(node_data)
return {
"status": "success",
"node": {
"id": node["unique_id"],
"path": node["path"],
"type": "CalendarMonth",
"label": node.get("title", "Calendar Month"),
"data": converted_data
}
}
@router.get("/get-default-node/{context}")
async def get_default_node(context: str, db_name: str, base_context: str | None = None) -> Dict[str, Any]:
"""Get the default node for a given context."""
try:
# Handle special cases for week and month
if context == 'week':
return get_default_node_week(db_name)
elif context == 'month':
return get_default_node_month(db_name)
# Map contexts to their default node queries
context_queries = {
# Base Contexts
'profile': """
MATCH (n:User)
RETURN n LIMIT 1
""",
'worker': """
MATCH (n)
WHERE n:SchoolAdmin OR n:Teacher OR n:Student OR n:Developer OR n:SuperAdmin
RETURN n LIMIT 1
""",
'calendar': """
MATCH (n:Calendar)
RETURN n LIMIT 1
""",
'teaching': """
MATCH (n:Teacher)
RETURN n LIMIT 1
""",
'school': """
MATCH (n:School)
RETURN n LIMIT 1
""",
'department': """
MATCH (n:Department)
RETURN n LIMIT 1
""",
'class': """
MATCH (n:Class)
RETURN n LIMIT 1
""",
# Extended Contexts - Overview queries for each base context
'overview': """
MATCH (n)
WHERE CASE $base_context
WHEN 'profile' THEN n:User
WHEN 'calendar' THEN n:Calendar
WHEN 'teaching' THEN n:Teacher
WHEN 'school' THEN n:School
WHEN 'department' THEN n:Department
WHEN 'class' THEN n:Class
ELSE false
END
RETURN n LIMIT 1
""",
# Extended Contexts - User
'settings': """
MATCH (n:User)
RETURN n LIMIT 1
""",
'history': """
MATCH (n:User)
RETURN n LIMIT 1
""",
'journal': """
MATCH (n:Journal)
RETURN n LIMIT 1
""",
'planner': """
MATCH (n:Planner)
RETURN n LIMIT 1
""",
# Extended Contexts - Calendar
'day': """
MATCH (n:CalendarDay)
WHERE date(n.date) = date()
RETURN n LIMIT 1
""",
'year': """
MATCH (n:CalendarYear)
WHERE n.year = toString(date().year)
RETURN n LIMIT 1
""",
# Extended Contexts - Teaching
'timetable': """
MATCH (n:UserTeacherTimetable)
RETURN n LIMIT 1
""",
'classes': """
MATCH (n:Class)
RETURN n LIMIT 1
""",
'lessons': """
MATCH (n:TimetableLesson)
RETURN n LIMIT 1
""",
# Extended Contexts - School
'departments': """
MATCH (n:Department)
RETURN n LIMIT 1
""",
'staff': """
MATCH (n:Teacher)
RETURN n LIMIT 1
""",
# Extended Contexts - Department
'teachers': """
MATCH (n:Teacher)
RETURN n LIMIT 1
""",
'subjects': """
MATCH (n:Subject)
RETURN n LIMIT 1
""",
# Extended Contexts - Class
'students': """
MATCH (n:Student)
RETURN n LIMIT 1
"""
}
if context not in context_queries:
raise HTTPException(status_code=400, detail=f"Invalid context: {context}")
query = context_queries[context]
with driver_tools.get_session(database=db_name) as session:
# For overview context, we need to pass the database name as a parameter
params = {'db_name': db_name, 'base_context': base_context} if context == 'overview' else {}
result = session.run(query, params)
record = result.single()
if not record:
raise HTTPException(
status_code=404,
detail=f"No default node found for context: {context}"
)
node = record["n"]
node_data = dict(node)
# Convert Neo4j types to JSON-serializable types
converted_data = convert_neo4j_values(node_data)
return {
"status": "success",
"node": {
"id": node["unique_id"],
"path": node["path"],
"type": list(node.labels)[0],
"label": node.get("title", ""),
"data": converted_data
}
}
except Exception as e:
logger.error(f"Error getting default node: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
+101
View File
@@ -0,0 +1,101 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_calendar_get_events'
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 modules.database.tools.neo4j_driver_tools as driver
from fastapi import APIRouter, HTTPException
import colorsys
import random
# Predefined vibrant color palette
BASE_COLORS = [
"#FF4136", "#FF851B", "#FFDC00", "#2ECC40", "#0074D9", "#B10DC9",
"#F012BE", "#FF6F61", "#7FDBFF", "#01FF70", "#001f3f", "#85144b",
"#39CCCC", "#3D9970", "#e74c3c", "#e67e22", "#f1c40f", "#2ecc71",
"#1abc9c", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60",
"#2980b9", "#8e44ad", "#2c3e50", "#d35400", "#c0392b", "#bdc3c7",
"#7f8c8d", "#00a86b", "#8B4513", "#4B0082", "#800000", "#1E90FF"
]
def generate_vibrant_color():
h = random.random()
s = 0.5 + random.random() * 0.5 # 0.5 to 1.0
v = 0.5 + random.random() * 0.5 # 0.5 to 1.0
r, g, b = [int(x * 255) for x in colorsys.hsv_to_rgb(h, s, v)]
return f"#{r:02x}{g:02x}{b:02x}"
# Extend the color palette
EXTENDED_COLOR_PALETTE = BASE_COLORS + [generate_vibrant_color() for _ in range(100)]
def get_subject_class_color(subject_class):
# Use a hash function to generate a unique number for each subject class
hash_value = hash(subject_class)
# Use the hash to select a color from the extended palette
color_index = hash_value % len(EXTENDED_COLOR_PALETTE)
color = EXTENDED_COLOR_PALETTE[color_index]
return color
router = APIRouter()
@router.get("/get_teacher_timetable_events")
async def get_teacher_timetable_events(
unique_id: str,
worker_db_name: str
):
logging.info(f"Getting timetable events for teacher {unique_id} from database {worker_db_name}")
neo_driver = driver.get_driver(db_name=worker_db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=worker_db_name) as neo_session:
query = """
MATCH (t:Teacher {unique_id: $unique_id})-[:TEACHER_HAS_TIMETABLE]->(tt:TeacherTimetable)
-[:TIMETABLE_HAS_CLASS]->(sc:SubjectClass)-[:CLASS_HAS_LESSON]->(tl:TimetableLesson)
RETURN tl.unique_id as id,
tl.period_code as period_code,
COALESCE(sc.subject_class_code, 'Untitled Class') as subject_class,
tl.date as date,
tl.start_time as start_time,
tl.end_time as end_time,
tl.path as path
"""
result = neo_session.run(query, unique_id=unique_id)
events = []
for record in result:
start = f"{record['date']}T{record['start_time']}"
end = f"{record['date']}T{record['end_time']}"
title = f"{record['subject_class']}"
events.append({
"id": record["id"],
"title": title,
"start": start,
"end": end,
"groupId": f"subject-class-{record['subject_class']}",
"extendedProps": {
"subjectClass": record['subject_class'],
"color": get_subject_class_color(record['subject_class']),
"periodCode": record['period_code'],
"path": record['path']
}
})
logging.info(f"Found {len(events)} events for teacher {unique_id}")
return {"status": "success", "events": events}
except Exception as e:
logging.error(f"Error fetching events: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
+563
View File
@@ -0,0 +1,563 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_tools_get_nodes'
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 modules.database.tools.neo4j_driver_tools as driver
import modules.database.tools.neo4j_session_tools as session
from modules.database.schemas.nodes.calendars import CalendarNode
from modules.database.schemas.nodes.schools.timetable import SchoolTimetableNode, AcademicYearNode, AcademicTermNode, AcademicWeekNode, AcademicDayNode, AcademicPeriodNode, RegistrationPeriodNode
from modules.database.schemas.nodes.users import UserNode
from modules.database.schemas.nodes.workers.workers import TeacherNode, StudentNode, DeveloperNode, SchoolAdminNode
from modules.database.schemas.nodes.schools.schools import SchoolNode, DepartmentNode, SubjectClassNode, RoomNode
from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode, UserTeacherTimetableNode
from fastapi import APIRouter, HTTPException, Query
router = APIRouter()
@router.get("/get-node")
async def get_node(unique_id: str = Query(...), db_name: str = Query(...)):
logging.info(f"Getting node for {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n {unique_id: $unique_id})
RETURN n
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
node = record['n']
node_labels = list(node.labels)
node_data = dict(node)
try:
# Convert node based on its type
node_type = node_labels[0] if node_labels else "Unknown"
if node_type in globals():
node_class = globals()[f"{node_type}Node"]
node_object = node_class(**node_data)
node_dict = node_object.to_dict()
else:
node_dict = node_data
return {
"status": "success",
"node": {
"node_type": node_type,
"node_data": node_dict
}
}
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {
"status": "error",
"message": "Error processing node data",
"details": str(e)
}
else:
return {"status": "not_found", "message": "Node not found"}
except Exception as e:
logging.error(f"Error retrieving node: {str(e)}")
return {"status": "error", "message": "Internal server error"}
finally:
driver.close_driver(neo_driver)
@router.get("/get-user-node")
async def get_user_node(user_id: str = Query(...)):
db_name = f"cc.users.{user_id}"
logging.info(f"Getting user node for user {user_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
nodes = session.find_nodes_by_label_and_properties(neo_session, "User", {"user_id": user_id})
if nodes:
user_node = nodes[0]
data = UserNode(**user_node)
user_node_data = data.to_dict()
return {"status": "success", "user_node": user_node_data, "user_node_raw": nodes}
else:
return {"status": "not_found", "message": "User node not found"}
except Exception as e:
logging.error(f"Error retrieving user node: {str(e)}")
return {"status": "error", "message": "Internal server error"}
finally:
driver.close_driver(neo_driver)
@router.get("/get-connected-nodes")
async def get_connected_nodes(unique_id: str = Query(...), db_name: str = Query(...)):
logging.info(f"Getting connected nodes for {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n {unique_id: $unique_id})
OPTIONAL MATCH (n)-[]-(connected)
RETURN n, collect(connected) as connected_nodes
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
main_node = record['n']
connected_nodes = record['connected_nodes']
main_node_labels = list(main_node.labels)
main_node_type = main_node_labels[0] if main_node_labels else "Unknown"
main_node_data = dict(main_node)
try:
main_node_class = globals()[f"{main_node_type}Node"]
main_node_object = main_node_class(**main_node_data)
main_node_dict = main_node_object.to_dict()
except Exception as e:
logging.error(f"Error converting main node to dict: {str(e)}")
main_node_dict = main_node_data
connected_nodes_list = []
for node in connected_nodes:
node_labels = list(node.labels)
node_type = node_labels[0] if node_labels else "Unknown"
node_data = dict(node)
try:
node_class = globals()[f"{node_type}Node"]
node_object = node_class(**node_data)
connected_node_dict = node_object.to_dict()
except Exception as e:
logging.error(f"Error converting connected node to dict: {str(e)}")
connected_node_dict = node_data
connected_node_info = {
"node_type": node_type,
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
logging.debug(f"connected_nodes_list: {connected_nodes_list}")
return {
"status": "success",
"main_node": {
"node_type": main_node_type,
"node_data": main_node_dict
},
"connected_nodes": connected_nodes_list
}
else:
return {"status": "not_found", "message": "Node not found"}
except Exception as e:
logging.error(f"Error retrieving connected nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-user-connected-nodes")
async def get_user_connected_nodes(unique_id: str = Query(...)):
logging.info(f"Getting user adjacent nodes for node {unique_id}")
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") # TODO: This function needs to be able to take a db_name as a parameter
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
raise HTTPException(status_code=500, detail="Failed to connect to the database")
try:
with neo_driver.session(database=db_name) as neo_session:
user_node_and_connected_nodes = session.get_node_by_unique_id_and_adjacent_nodes(neo_session, unique_id)
user_node = user_node_and_connected_nodes['node']
connected_nodes = user_node_and_connected_nodes['connected_nodes']
try:
data = UserNode(**user_node)
user_node_dict = data.to_dict()
except Exception as e:
logging.error(f"Error converting user node to dict: {str(e)}")
connected_nodes_list = []
for connected_node in connected_nodes:
node_data = connected_node['node']
node_labels = list(node_data.labels)
logging.debug(f"node_labels: {node_labels}")
for label in node_labels:
logging.debug(f"label: {label}")
try:
if 'Developer' == label:
logging.debug(f"Developer node found")
node_object = DeveloperNode(**node_data)
elif 'SchoolAdmin' == label:
logging.debug(f"SchoolAdmin node found")
node_object = SchoolAdminNode(**node_data)
elif 'Teacher' == label:
logging.debug(f"Teacher node found")
node_object = TeacherNode(**node_data)
elif 'Student' == label:
logging.debug(f"Student node found")
node_object = StudentNode(**node_data)
elif 'Calendar' == label:
logging.debug(f"Calendar node found")
node_object = CalendarNode(**node_data)
elif 'TeacherTimetable' == label:
logging.debug(f"TeacherTimetable node found")
node_object = TeacherTimetableNode(**node_data)
elif 'UserTeacherTimetable' == label:
logging.debug(f"UserTeacherTimetable node found")
node_object = UserTeacherTimetableNode(**node_data)
elif 'School' == label:
logging.debug(f"School node found")
node_object = SchoolNode(**node_data)
elif 'Department' == label:
logging.debug(f"Department node found")
node_object = DepartmentNode(**node_data)
elif 'Student' == label:
logging.debug(f"Student node found")
node_object = StudentNode(**node_data)
elif 'Class' == label:
logging.debug(f"Class node found")
node_object = SubjectClassNode(**node_data)
elif 'Room' == label:
logging.debug(f"Room node found")
node_object = RoomNode(**node_data)
else:
logging.error(f"Unknown node label: {node_labels}")
continue
connected_node_dict = node_object.to_dict()
logging.debug(f"connected_node_dict: {connected_node_dict}")
connected_node_info = {
"node_type": label,
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {"status": "success", "user_node": user_node_dict, "user_connected_nodes": connected_nodes_list}
except Exception as e:
logging.error(f"Error retrieving adjacent nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-worker-connected-nodes")
async def get_worker_connected_nodes(unique_id: str = Query(...)):
logging.info(f"Getting worker adjacent nodes for node {unique_id}")
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") # TODO: This function needs to be able to take a db_name as a parameter
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
raise HTTPException(status_code=500, detail="Failed to connect to the database")
try:
with neo_driver.session(database=db_name) as neo_session:
node_and_connected_nodes = session.get_node_by_unique_id_and_adjacent_nodes(neo_session, unique_id)
worker_node = node_and_connected_nodes['node']
connected_nodes = node_and_connected_nodes['connected_nodes']
try:
data = TeacherNode(**worker_node)
worker_node_dict = data.to_dict()
except Exception as e:
logging.error(f"Error converting user node to dict: {str(e)}")
connected_nodes_list = []
for connected_node in connected_nodes:
node_data = connected_node['node']
node_labels = list(node_data.labels)
logging.debug(f"node_labels: {node_labels}")
for label in node_labels:
logging.debug(f"label: {label}")
try:
if 'Calendar' == label:
logging.debug(f"Calendar node found")
node_object = CalendarNode(**node_data)
elif 'TeacherTimetable' == label:
logging.debug(f"TeacherTimetable node found")
node_object = TeacherTimetableNode(**node_data)
elif 'UserTeacherTimetable' == label:
logging.debug(f"UserTeacherTimetable node found")
node_object = UserTeacherTimetableNode(**node_data)
elif 'School' == label:
logging.debug(f"School node found")
node_object = SchoolNode(**node_data)
elif 'Department' == label:
logging.debug(f"Department node found")
node_object = DepartmentNode(**node_data)
elif 'Student' == label:
logging.debug(f"Student node found")
node_object = StudentNode(**node_data)
elif 'Class' == label:
logging.debug(f"Class node found")
node_object = SubjectClassNode(**node_data)
elif 'Room' == label:
logging.debug(f"Room node found")
node_object = RoomNode(**node_data)
else:
logging.error(f"Unknown node label: {node_labels}")
continue
connected_node_dict = node_object.to_dict()
logging.debug(f"connected_node_dict: {connected_node_dict}")
connected_node_info = {
"node_type": label,
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {"status": "success", "user_node": worker_node_dict, "worker_connected_nodes": connected_nodes_list}
except Exception as e:
logging.error(f"Error retrieving worker adjacent nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-calendar-connected-nodes")
async def get_calendar_connected_nodes(unique_id: str = Query(...)):
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai")
logging.info(f"Getting connected nodes for calendar {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n)
WHERE n.unique_id = $unique_id AND (n:Calendar OR n:CalendarYear OR n:CalendarMonth OR n:CalendarWeek OR n:CalendarDay OR n:CalendarTimeChunk)
OPTIONAL MATCH (n)-[]-(connected)
RETURN n, collect(connected) as connected_nodes
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
calendar_node = record['n']
connected_nodes = record['connected_nodes']
node_type = list(calendar_node.labels)[0]
calendar_dict = globals()[f"{node_type}Node"](**calendar_node).to_dict()
connected_nodes_list = []
for node in connected_nodes:
node_labels = list(node.labels)
node_data = dict(node)
try:
node_class = globals()[f"{node_labels[0]}Node"]
node_object = node_class(**node_data)
connected_node_dict = node_object.to_dict()
connected_node_info = {
"node_type": node_labels[0],
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {"status": "success", "calendar_node": calendar_dict, "connected_nodes": connected_nodes_list}
else:
return {"status": "not_found", "message": "Calendar node not found"}
except Exception as e:
logging.error(f"Error retrieving connected nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-teacher-timetable-connected-nodes")
async def get_teacher_timetable_connected_nodes(unique_id: str = Query(...)):
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai")
logging.info(f"Getting connected nodes for teacher timetable {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n:TeacherTimetable {unique_id: $unique_id})
OPTIONAL MATCH (n)-[]-(connected)
RETURN n, collect(connected) as connected_nodes
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
teacher_timetable_node = record['n']
connected_nodes = record['connected_nodes']
teacher_timetable_dict = TeacherTimetableNode(**teacher_timetable_node).to_dict()
connected_nodes_list = []
for node in connected_nodes:
node_labels = list(node.labels)
node_data = dict(node)
try:
if 'TimetableLesson' in node_labels:
node_object = TimetableLessonNode(**node_data)
elif 'PlannedLesson' in node_labels:
node_object = PlannedLessonNode(**node_data)
else:
logging.error(f"Unknown node label: {node_labels}")
continue
connected_node_dict = node_object.to_dict()
connected_node_info = {
"node_type": node_labels[0],
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {"status": "success", "teacher_timetable_node": teacher_timetable_dict, "connected_nodes": connected_nodes_list}
else:
return {"status": "not_found", "message": "Teacher timetable node not found"}
except Exception as e:
logging.error(f"Error retrieving connected nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-school-timetable-connected-nodes")
async def get_school_timetable_connected_nodes(unique_id: str = Query(...)):
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai")
logging.info(f"Getting connected nodes for school timetable {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n:SchoolTimetable {unique_id: $unique_id})
OPTIONAL MATCH (n)-[]-(connected)
RETURN n, collect(connected) as connected_nodes
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
school_timetable_node = record['n']
connected_nodes = record['connected_nodes']
school_timetable_dict = SchoolTimetableNode(**school_timetable_node).to_dict()
connected_nodes_list = []
for node in connected_nodes:
node_labels = list(node.labels)
node_data = dict(node)
try:
if 'AcademicYear' in node_labels:
node_object = AcademicYearNode(**node_data)
elif 'AcademicTerm' in node_labels:
node_object = AcademicTermNode(**node_data)
elif 'AcademicWeek' in node_labels:
node_object = AcademicWeekNode(**node_data)
elif 'AcademicDay' in node_labels:
node_object = AcademicDayNode(**node_data)
elif 'AcademicPeriod' in node_labels:
node_object = AcademicPeriodNode(**node_data)
elif 'RegistrationPeriod' in node_labels:
node_object = RegistrationPeriodNode(**node_data)
else:
logging.error(f"Unknown node label: {node_labels}")
continue
connected_node_dict = node_object.to_dict()
connected_node_info = {
"node_type": node_labels[0],
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {"status": "success", "school_timetable_node": school_timetable_dict, "connected_nodes": connected_nodes_list}
else:
return {"status": "not_found", "message": "School timetable node not found"}
except Exception as e:
logging.error(f"Error retrieving connected nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-curriculum-connected-nodes")
async def get_curriculum_connected_nodes(unique_id: str = Query(...)):
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai")
logging.info(f"Getting connected nodes for curriculum {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n)
WHERE n.unique_id = $unique_id AND (n:PastoralStructure OR n:YearGroup OR n:CurriculumStructure OR n:KeyStage OR n:KeyStageSyllabus OR n:YearGroupSyllabus OR n:Subject OR n:Topic OR n:TopicLesson OR n:LearningStatement OR n:ScienceLab)
OPTIONAL MATCH (n)-[]-(connected)
RETURN n, collect(connected) as connected_nodes
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
curriculum_node = record['n']
connected_nodes = record['connected_nodes']
node_type = list(curriculum_node.labels)[0]
curriculum_dict = globals()[f"{node_type}Node"](**curriculum_node).to_dict()
connected_nodes_list = []
for node in connected_nodes:
node_labels = list(node.labels)
node_data = dict(node)
try:
node_class = globals()[f"{node_labels[0]}Node"]
node_object = node_class(**node_data)
connected_node_dict = node_object.to_dict()
connected_node_info = {
"node_type": node_labels[0],
"node_data": connected_node_dict
}
connected_nodes_list.append(connected_node_info)
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
return {"status": "success", "curriculum_node": curriculum_dict, "connected_nodes": connected_nodes_list}
else:
return {"status": "not_found", "message": "Curriculum node not found"}
except Exception as e:
logging.error(f"Error retrieving connected nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@router.get("/get-school-node")
async def get_school_node(school_uuid: str = Query(...)):
logging.info(f"Getting school node for school {school_uuid}...")
db_name = f"cc.institutes.{school_uuid}"
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
nodes = session.find_nodes_by_label_and_properties(neo_session, "School", {"school_uuid": school_uuid})
if nodes:
school_node = nodes[0]
data = SchoolNode(
unique_id=school_node["unique_id"],
school_uuid=school_node["school_uuid"],
school_name=school_node["school_name"],
school_website=school_node["school_website"],
path=school_node["path"]
)
school_node_data = data.to_dict()
return {"status": "success", "school_node": school_node_data, "school_node_raw": nodes}
else:
return {"status": "not_found", "message": "School node not found"}
except Exception as e:
logging.error(f"Error retrieving school node: {str(e)}")
return {"status": "error", "message": "Internal server error"}
finally:
driver.close_driver(neo_driver)
@@ -0,0 +1,174 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_tools_get_nodes'
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 modules.database.tools.neo4j_driver_tools as driver
import modules.database.tools.neo4j_session_tools as session
from modules.database.schemas.nodes.calendars import CalendarNode, CalendarYearNode, CalendarMonthNode, CalendarWeekNode, CalendarDayNode, CalendarTimeChunkNode
from modules.database.schemas.nodes.users import UserNode
from modules.database.schemas.nodes.workers.workers import TeacherNode, StudentNode, DeveloperNode, SchoolAdminNode
from modules.database.schemas.nodes.structures.schools import PastoralStructureNode, CurriculumStructureNode
from modules.database.schemas.nodes.schools.pastoral import YearGroupNode, YearGroupSyllabusNode
from modules.database.schemas.nodes.schools.curriculum import SubjectNode, TopicNode, TopicLessonNode, LearningStatementNode, ScienceLabNode
from modules.database.schemas.nodes.schools.timetable import SchoolTimetableNode, AcademicYearNode, AcademicTermNode, AcademicWeekNode, AcademicDayNode, OffTimetableDayNode, StaffDayNode, AcademicPeriodNode, RegistrationPeriodNode, OffTimetablePeriodNode, AcademicTermBreakNode, BreakPeriodNode, HolidayDayNode, HolidayWeekNode
from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode, UserTeacherTimetableNode, StudentTimetableNode, SchoolAdminTimetableNode, DeveloperTimetableNode, SuperAdminTimetableNode
from modules.database.schemas.nodes.schools.schools import SchoolNode, DepartmentNode, SubjectClassNode, RoomNode
from fastapi import APIRouter, HTTPException, Query
router = APIRouter()
@router.get("/get-all-nodes-and-edges")
async def get_all_nodes_and_edges():
db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai")
logging.info(f"Getting all nodes and edges from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n)-[r]->(m)
RETURN n, r, m
"""
result = neo_session.run(query)
nodes = {}
relationships = []
for record in result:
source = record['n']
target = record['m']
relationship = record['r']
for node in [source, target]:
if node.id not in nodes:
node_labels = list(node.labels)
node_type = node_labels[0] if node_labels else "Unknown"
node_data = dict(node)
try:
node_class = globals()[f"{node_type}Node"]
node_object = node_class(**node_data)
node_dict = node_object.to_dict()
except Exception as e:
logging.error(f"Error converting node to dict: {str(e)}")
node_dict = node_data
nodes[node.id] = {
"node_type": node_type,
"node_data": node_dict
}
relationship_info = {
"start_node": source.id,
"end_node": target.id,
"relationship_type": relationship.type,
"relationship_properties": dict(relationship)
}
relationships.append(relationship_info)
return {
"status": "success",
"nodes": list(nodes.values()),
"relationships": relationships
}
except Exception as e:
logging.error(f"Error retrieving all nodes and edges: {str(e)}")
return {"status": "error", "message": "Internal server error"}
finally:
driver.close_driver(neo_driver)
@router.get("/get-connected-nodes-and-edges")
async def get_connected_nodes_and_edges(unique_id: str = Query(...), db_name: str = Query(...)):
logging.info(f"Getting connected nodes and edges for {unique_id} from database {db_name}")
neo_driver = driver.get_driver(db_name=db_name)
if neo_driver is None:
return {"status": "error", "message": "Failed to connect to the database"}
try:
with neo_driver.session(database=db_name) as neo_session:
query = """
MATCH (n {unique_id: $unique_id})
OPTIONAL MATCH (n)-[r]-(connected)
RETURN n, collect(connected) as connected_nodes, collect(r) as relationships
"""
result = neo_session.run(query, unique_id=unique_id)
record = result.single()
if record:
main_node = record['n']
connected_nodes = record['connected_nodes']
relationships = record['relationships']
main_node_labels = list(main_node.labels)
main_node_type = main_node_labels[0] if main_node_labels else "Unknown"
main_node_data = dict(main_node)
try:
main_node_class = globals()[f"{main_node_type}Node"]
main_node_object = main_node_class(**main_node_data)
main_node_dict = main_node_object.to_dict()
except Exception as e:
logging.error(f"Error converting main node to dict: {str(e)}")
main_node_dict = main_node_data
connected_nodes_list = []
relationship_list = []
for node, relationship in zip(connected_nodes, relationships):
node_labels = list(node.labels)
node_type = node_labels[0] if node_labels else "Unknown"
node_data = dict(node)
try:
node_class = globals()[f"{node_type}Node"]
node_object = node_class(**node_data)
connected_node_dict = node_object.to_dict()
except Exception as e:
logging.error(f"Error converting connected node to dict: {str(e)}")
connected_node_dict = node_data
connected_node_info = {
"node_type": node_type,
"node_data": connected_node_dict,
"relationship_type": relationship.type, # Get relationship type
"relationship_properties": dict(relationship) # Relationship properties, if any
}
connected_nodes_list.append(connected_node_info)
relationship_info = {
"start_node": dict(relationship.start_node),
"end_node": dict(relationship.end_node),
"relationship_type": relationship.type,
"relationship_properties": dict(relationship)
}
relationship_list.append(relationship_info)
logging.info(f"Main node: {main_node_dict}")
logging.info(f"Connected nodes: {connected_nodes_list}")
logging.info(f"Relationships: {relationship_list}")
return {
"status": "success",
"main_node": {
"node_type": main_node_type,
"node_data": main_node_dict
},
"connected_nodes": connected_nodes_list,
"relationships": relationship_list
}
else:
return {"status": "not_found", "message": "Node not found"}
except Exception as e:
logging.error(f"Error retrieving connected nodes: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
driver.close_driver(neo_driver)
@@ -0,0 +1,3 @@
import os
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
+196
View File
@@ -0,0 +1,196 @@
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import modules.logger_tool as logger
log_name = 'api_routers_database_tools_tldraw_filesystem'
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'
)
from fastapi import APIRouter, HTTPException, Query
from typing import Dict
import json
from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem
from modules.database.schemas.nodes.users import UserNode
from modules.database.tools.neo4j_db_formatter import format_user_email_for_neo_db
router = APIRouter()
@router.post("/get_tldraw_user_node_file")
async def read_tldraw_user_node_file(user_node: UserNode):
logging.debug(f"Reading tldraw file for user node: {user_node.user_email}")
# Format the database name using the email
formatted_email = format_user_email_for_neo_db(user_node.user_email)
db_name = f"cc.users.{formatted_email}"
fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user")
logging.debug(f"Filesystem root path: {fs.root_path}")
# Handle path based on environment
if os.getenv("DEV_MODE") == "true":
# In dev mode, use the full system path from the node
if not user_node.path:
raise HTTPException(status_code=400, detail="Node path not found")
logging.debug(f"Using DEV_MODE path: {user_node.path}")
base_path = os.path.normpath(user_node.path)
else:
# In prod mode, construct path using formatted email
logging.warning(f"Using db_name as base path not ready in prod: {db_name}")
base_path = formatted_email
# Construct final path including tldraw file
logging.debug(f"Base path: {base_path}")
file_path = os.path.join(base_path, "tldraw_file.json")
logging.debug(f"File path: {file_path}")
file_location = os.path.normpath(os.path.join(fs.root_path, file_path))
logging.debug(f"File location: {file_location}")
logging.debug(f"Attempting to read file at: {file_location}")
if os.path.exists(file_location):
logging.debug(f"File exists: {file_location}")
try:
with open(file_location, "r") as file:
data = json.load(file)
return data
except json.JSONDecodeError as e:
logging.error(f"Failed to parse JSON from file: {e}")
raise HTTPException(status_code=500, detail="Invalid JSON in file")
except Exception as e:
logging.error(f"Error reading file: {e}")
raise HTTPException(status_code=500, detail="Error reading file")
else:
logging.debug(f"File does not exist: {file_location}")
raise HTTPException(status_code=404, detail="File not found")
@router.post("/set_tldraw_user_node_file")
async def set_tldraw_user_node_file(user_node: UserNode, data: Dict):
logging.debug(f"Setting tldraw file for user node: {user_node.user_email}")
# Format the database name using the email
formatted_email = format_user_email_for_neo_db(user_node.user_email)
db_name = f"cc.users.{formatted_email}"
fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user")
# Handle path based on environment
if os.getenv("ENVIRONMENT") == "dev":
# In dev mode, use the full system path from the node
if not user_node.path:
raise HTTPException(status_code=400, detail="Node path not found")
base_path = os.path.normpath(user_node.path)
else:
# In prod mode, construct path using formatted email
base_path = formatted_email
# Construct final path including tldraw file
file_path = os.path.join(base_path, "tldraw_file.json")
file_location = os.path.normpath(os.path.join(fs.root_path, file_path))
logging.debug(f"Attempting to write file at: {file_location}")
try:
# Ensure directory exists
os.makedirs(os.path.dirname(file_location), exist_ok=True)
# Write the file
with open(file_location, "w") as file:
json.dump(data, file)
return {"status": "success"}
except Exception as e:
logging.error(f"Error writing file: {e}")
raise HTTPException(status_code=500, detail="Error writing file")
@router.get("/get_tldraw_node_file")
async def read_tldraw_node_file(path: str, db_name: str):
logging.debug(f"Reading tldraw file for path: {path}")
fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user")
logging.debug(f"Filesystem root path: {fs.root_path}")
# Handle path based on environment
if os.getenv("DEV_MODE") == "true":
# In dev mode, use the full system path from the node
if not path:
raise HTTPException(status_code=400, detail="Path not provided")
logging.debug(f"Using DEV_MODEpath: {path}")
base_path = os.path.normpath(path)
else:
# In prod mode, construct path
logging.warning(f"Using db_name as base path not ready in prod: {db_name}")
base_path = db_name
# Construct final path including tldraw file
logging.debug(f"Base path: {base_path}")
file_path = os.path.join(base_path, "tldraw_file.json")
logging.debug(f"File path: {file_path}")
file_location = os.path.normpath(os.path.join(fs.root_path, file_path))
logging.debug(f"File location: {file_location}")
logging.debug(f"Attempting to read file at: {file_location}")
if os.path.exists(file_location):
logging.debug(f"File exists: {file_location}")
try:
with open(file_location, "r") as file:
data = json.load(file)
return data
except json.JSONDecodeError as e:
logging.error(f"Failed to parse JSON from file: {e}")
raise HTTPException(status_code=500, detail="Invalid JSON in file")
except Exception as e:
logging.error(f"Error reading file: {e}")
raise HTTPException(status_code=500, detail="Error reading file")
else:
logging.debug(f"File does not exist: {file_location}")
raise HTTPException(status_code=404, detail="File not found")
@router.post("/set_tldraw_node_file")
async def set_tldraw_node_file(path: str, db_name: str, data: Dict):
logging.debug(f"Setting tldraw file for path: {path}")
fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user")
logging.debug(f"Filesystem root path: {fs.root_path}")
# Handle path based on environment
if os.getenv("DEV_MODE") == "true":
# In dev mode, use the full system path from the node
if not path:
raise HTTPException(status_code=400, detail="Path not provided")
logging.debug(f"Using DEV_MODEpath: {path}")
base_path = os.path.normpath(path)
else:
# In prod mode, construct path
logging.warning(f"Using db_name as base path not ready in prod: {db_name}")
base_path = db_name
# Construct final path including tldraw file
logging.debug(f"Base path: {base_path}")
file_path = os.path.join(base_path, "tldraw_file.json")
logging.debug(f"File path: {file_path}")
file_location = os.path.normpath(os.path.join(fs.root_path, file_path))
logging.debug(f"File location: {file_location}")
logging.debug(f"Attempting to set file at: {file_location}")
try:
# Ensure directory exists
os.makedirs(os.path.dirname(file_location), exist_ok=True)
# Write the file
with open(file_location, "w") as file:
json.dump(data, file)
return {"status": "success"}
except Exception as e:
logging.error(f"Error writing file: {e}")
raise HTTPException(status_code=500, detail="Error writing file")
@@ -0,0 +1,190 @@
import os
from fastapi import APIRouter, HTTPException
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from modules.logger_tool import initialise_logger
from modules.database.tools import neo4j_driver_tools as driver_tools
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)
router = APIRouter()
@router.get("/get-worker-structure")
async def get_worker_structure(db_name: str) -> Dict[str, Any]:
"""
Get the complete worker structure including timetables, classes, lessons, journals, and planners.
"""
try:
# Get all worker-related nodes in a single query
query = """
// Match all worker-related nodes
MATCH (t:Teacher)
OPTIONAL MATCH (t)-[:TEACHER_HAS_TIMETABLE]->(tt:UserTeacherTimetable)
OPTIONAL MATCH (t)-[:TEACHER_HAS_CLASS]->(c:Class)
OPTIONAL MATCH (t)-[:TEACHER_HAS_LESSON]->(l:TimetableLesson)
OPTIONAL MATCH (t)-[:TEACHER_HAS_JOURNAL]->(j:Journal)
OPTIONAL MATCH (t)-[:TEACHER_HAS_PLANNER]->(p:Planner)
WITH t, tt, c, l, j, p
ORDER BY tt.start_date, c.created, l.created, j.created, p.created
// Collect all nodes
RETURN {
timetables: collect(DISTINCT {
id: tt.unique_id,
path: tt.path,
title: tt.title,
type: tt.__primarylabel__,
startTime: toString(tt.start_date),
endTime: toString(tt.end_date)
}),
classes: collect(DISTINCT {
id: c.unique_id,
path: c.path,
title: c.title,
type: c.__primarylabel__
}),
lessons: collect(DISTINCT {
id: l.unique_id,
path: l.path,
title: l.title,
type: l.__primarylabel__
}),
journals: collect(DISTINCT {
id: j.unique_id,
path: j.path,
title: j.title,
type: j.__primarylabel__
}),
planners: collect(DISTINCT {
id: p.unique_id,
path: p.path,
title: p.title,
type: p.__primarylabel__
})
} as structure
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
record = result.single()
if not record:
raise HTTPException(status_code=404, detail="Worker structure not found")
structure = record["structure"]
return {
"status": "success",
"data": {
"timetables": {
"default": structure["timetables"]
},
"classes": {
"default": structure["classes"]
},
"lessons": {
"default": structure["lessons"]
},
"journals": {
"default": structure["journals"]
},
"planners": {
"default": structure["planners"]
}
}
}
except Exception as e:
logger.error(f"Error getting worker structure: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-timetables")
async def get_timetables(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]:
"""
Get all timetables in a date range.
"""
try:
query = """
MATCH (tt:UserTeacherTimetable)
WHERE date(tt.start_date) >= date($start_date) AND date(tt.end_date) <= date($end_date)
RETURN {
id: tt.unique_id,
path: tt.path,
title: tt.title,
type: tt.__primarylabel__,
startTime: toString(tt.start_date),
endTime: toString(tt.end_date)
} as timetable
ORDER BY tt.start_date
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query, start_date=start_date, end_date=end_date)
timetables = [record["timetable"] for record in result]
return {
"status": "success",
"timetables": timetables
}
except Exception as e:
logger.error(f"Error getting timetables: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-journals")
async def get_journals(db_name: str) -> Dict[str, Any]:
"""
Get all journals.
"""
try:
query = """
MATCH (j:Journal)
RETURN {
id: j.unique_id,
path: j.path,
title: j.title,
type: j.__primarylabel__
} as journal
ORDER BY j.created
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
journals = [record["journal"] for record in result]
return {
"status": "success",
"journals": journals
}
except Exception as e:
logger.error(f"Error getting journals: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/get-planners")
async def get_planners(db_name: str) -> Dict[str, Any]:
"""
Get all planners.
"""
try:
query = """
MATCH (p:Planner)
RETURN {
id: p.unique_id,
path: p.path,
title: p.title,
type: p.__primarylabel__
} as planner
ORDER BY p.created
"""
with driver_tools.get_session(database=db_name) as session:
result = session.run(query)
planners = [record["planner"] for record in result]
return {
"status": "success",
"planners": planners
}
except Exception as e:
logger.error(f"Error getting planners: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))