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_uuid_string 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(...), school_uuid_string: str = Form(...), school_name: str = Form(...), school_website: str = Form(...), school_node_storage_path: str = Form(...) ): school_node = SchoolNode( uuid_string=school_uuid_string, name=school_name, website=school_website, node_storage_path=school_node_storage_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['uuid_string']} 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_uuid_string(session=neo_session, uuid_string=worker_node_data['uuid_string']) if school_worker_node is None: error_msg = f"School worker node not found for uuid_string: {worker_node_data['uuid_string']}" 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( uuid_string=worker_node_data['uuid_string'], teacher_code=worker_node_data['teacher_code'], teacher_name_formal=worker_node_data['teacher_name_formal'], teacher_email=worker_node_data['teacher_email'], node_storage_path=worker_node_data['node_storage_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( uuid_string=user_node_data['uuid_string'], 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'], node_storage_path=user_node_data['node_storage_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) @router.get("/current-period") async def get_current_period(user_id: str = ""): """Get the current active timetable period for a teacher. Queries Neo4j for Academic or Registration periods where now() falls between start_time and end_time for the given teacher's uuid_string. """ if not user_id: return { "period_id": None, "event_type": None, "event_label": None, "start_time": None, "end_time": None, } # The user_id from Supabase JWT maps to the TeacherNode's uuid_string teacher_uuid = user_id # Try to find the current period across all known databases # First, try to get the user's database name from the UserNode neo_driver = driver.get_driver() if neo_driver is None: logging.error("Failed to connect to Neo4j for current-period query") return { "period_id": None, "event_type": None, "event_label": None, "start_time": None, "end_time": None, } try: # Query for the current period # Look for Teacher nodes with matching uuid_string that have relationships to Academic/Registration periods query = """ MATCH (t:Teacher {uuid_string: $teacher_uuid})-[:HAS_PERIOD]->(p:AcademicPeriod|RegistrationPeriod) WHERE p.start_time <= datetime() AND p.end_time >= datetime() RETURN p.uuid_string AS period_id, p.name AS event_label, p.start_time AS start_time, p.end_time AS end_time, CASE WHEN p:AcademicPeriod THEN 'lesson' ELSE 'registration' END AS event_type ORDER BY p.start_time ASC LIMIT 1 """ with neo_driver.session() as session: result = session.run(query, teacher_uuid=teacher_uuid) record = result.single() if record: logging.info(f"Found current period for teacher {teacher_uuid}: {record['event_label']}") return { "period_id": record["period_id"], "event_type": record["event_type"], "event_label": record["event_label"], "start_time": str(record["start_time"]) if record["start_time"] else None, "end_time": str(record["end_time"]) if record["end_time"] else None, } else: logging.info(f"No current period found for teacher {teacher_uuid}") return { "period_id": None, "event_type": None, "event_label": None, "start_time": None, "end_time": None, } except Exception as e: logging.error(f"Error querying current period: {str(e)}") return { "period_id": None, "event_type": None, "event_label": None, "start_time": None, "end_time": None, } finally: driver.close_driver(neo_driver)